mediaQueue;
private final QueuePositionListener queuePositionListener;
+ private final ConcatenatingMediaSource concatenatingMediaSource;
- private DynamicConcatenatingMediaSource dynamicConcatenatingMediaSource;
private boolean castMediaQueueCreationPending;
private int currentItemIndex;
private Player currentPlayer;
@@ -117,9 +117,10 @@ import java.util.ArrayList;
this.castControlView = castControlView;
mediaQueue = new ArrayList<>();
currentItemIndex = C.INDEX_UNSET;
+ concatenatingMediaSource = new ConcatenatingMediaSource();
DefaultTrackSelector trackSelector = new DefaultTrackSelector(BANDWIDTH_METER);
- RenderersFactory renderersFactory = new DefaultRenderersFactory(context, null);
+ RenderersFactory renderersFactory = new DefaultRenderersFactory(context);
exoPlayer = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector);
exoPlayer.addListener(this);
localPlayerView.setPlayer(exoPlayer);
@@ -155,9 +156,8 @@ import java.util.ArrayList;
*/
public void addItem(Sample sample) {
mediaQueue.add(sample);
- if (currentPlayer == exoPlayer) {
- dynamicConcatenatingMediaSource.addMediaSource(buildMediaSource(sample));
- } else {
+ concatenatingMediaSource.addMediaSource(buildMediaSource(sample));
+ if (currentPlayer == castPlayer) {
castPlayer.addItems(buildMediaQueueItem(sample));
}
}
@@ -186,9 +186,8 @@ import java.util.ArrayList;
* @return Whether the removal was successful.
*/
public boolean removeItem(int itemIndex) {
- if (currentPlayer == exoPlayer) {
- dynamicConcatenatingMediaSource.removeMediaSource(itemIndex);
- } else {
+ concatenatingMediaSource.removeMediaSource(itemIndex);
+ if (currentPlayer == castPlayer) {
if (castPlayer.getPlaybackState() != Player.STATE_IDLE) {
Timeline castTimeline = castPlayer.getCurrentTimeline();
if (castTimeline.getPeriodCount() <= itemIndex) {
@@ -215,9 +214,8 @@ import java.util.ArrayList;
*/
public boolean moveItem(int fromIndex, int toIndex) {
// Player update.
- if (currentPlayer == exoPlayer) {
- dynamicConcatenatingMediaSource.moveMediaSource(fromIndex, toIndex);
- } else if (castPlayer.getPlaybackState() != Player.STATE_IDLE) {
+ concatenatingMediaSource.moveMediaSource(fromIndex, toIndex);
+ if (currentPlayer == castPlayer && castPlayer.getPlaybackState() != Player.STATE_IDLE) {
Timeline castTimeline = castPlayer.getCurrentTimeline();
int periodCount = castTimeline.getPeriodCount();
if (periodCount <= fromIndex || periodCount <= toIndex) {
@@ -263,6 +261,7 @@ import java.util.ArrayList;
public void release() {
currentItemIndex = C.INDEX_UNSET;
mediaQueue.clear();
+ concatenatingMediaSource.clear();
castPlayer.setSessionAvailabilityListener(null);
castPlayer.release();
localPlayerView.setPlayer(null);
@@ -354,11 +353,7 @@ import java.util.ArrayList;
// Media queue management.
castMediaQueueCreationPending = currentPlayer == castPlayer;
if (currentPlayer == exoPlayer) {
- dynamicConcatenatingMediaSource = new DynamicConcatenatingMediaSource();
- for (int i = 0; i < mediaQueue.size(); i++) {
- dynamicConcatenatingMediaSource.addMediaSource(buildMediaSource(mediaQueue.get(i)));
- }
- exoPlayer.prepare(dynamicConcatenatingMediaSource);
+ exoPlayer.prepare(concatenatingMediaSource);
}
// Playback transition.
diff --git a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java
index e51c5e89b7..4fab1966fe 100644
--- a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java
+++ b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java
@@ -17,8 +17,6 @@ package com.google.android.exoplayer2.imademo;
import android.content.Context;
import android.net.Uri;
-import android.os.Handler;
-import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.C.ContentType;
import com.google.android.exoplayer2.ExoPlayer;
@@ -27,7 +25,6 @@ import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.ext.ima.ImaAdsLoader;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
-import com.google.android.exoplayer2.source.MediaSourceEventListener;
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
import com.google.android.exoplayer2.source.dash.DashMediaSource;
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
@@ -83,8 +80,7 @@ import com.google.android.exoplayer2.util.Util;
// This is the MediaSource representing the content media (i.e. not the ad).
String contentUrl = context.getString(R.string.content_url);
- MediaSource contentMediaSource =
- buildMediaSource(Uri.parse(contentUrl), /* handler= */ null, /* listener= */ null);
+ MediaSource contentMediaSource = buildMediaSource(Uri.parse(contentUrl));
// Compose the content media source into a new AdsMediaSource with both ads and content.
MediaSource mediaSourceWithAds =
@@ -121,9 +117,8 @@ import com.google.android.exoplayer2.util.Util;
// AdsMediaSource.MediaSourceFactory implementation.
@Override
- public MediaSource createMediaSource(
- Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) {
- return buildMediaSource(uri, handler, listener);
+ public MediaSource createMediaSource(Uri uri) {
+ return buildMediaSource(uri);
}
@Override
@@ -134,25 +129,22 @@ import com.google.android.exoplayer2.util.Util;
// Internal methods.
- private MediaSource buildMediaSource(
- Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) {
+ private MediaSource buildMediaSource(Uri uri) {
@ContentType int type = Util.inferContentType(uri);
switch (type) {
case C.TYPE_DASH:
return new DashMediaSource.Factory(
new DefaultDashChunkSource.Factory(mediaDataSourceFactory),
manifestDataSourceFactory)
- .createMediaSource(uri, handler, listener);
+ .createMediaSource(uri);
case C.TYPE_SS:
return new SsMediaSource.Factory(
new DefaultSsChunkSource.Factory(mediaDataSourceFactory), manifestDataSourceFactory)
- .createMediaSource(uri, handler, listener);
+ .createMediaSource(uri);
case C.TYPE_HLS:
- return new HlsMediaSource.Factory(mediaDataSourceFactory)
- .createMediaSource(uri, handler, listener);
+ return new HlsMediaSource.Factory(mediaDataSourceFactory).createMediaSource(uri);
case C.TYPE_OTHER:
- return new ExtractorMediaSource.Factory(mediaDataSourceFactory)
- .createMediaSource(uri, handler, listener);
+ return new ExtractorMediaSource.Factory(mediaDataSourceFactory).createMediaSource(uri);
default:
throw new IllegalStateException("Unsupported type: " + type);
}
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/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json
index 7052e7c436..0d26f196c1 100644
--- a/demos/main/src/main/assets/media.exolist.json
+++ b/demos/main/src/main/assets/media.exolist.json
@@ -578,5 +578,16 @@
"ad_tag_uri": "http://vastsynthesizer.appspot.com/empty-midroll-2"
}
]
+ },
+ {
+ "name": "ABR",
+ "samples": [
+ {
+ "name": "Random ABR - Google Glass (MP4,H264)",
+ "uri": "http://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=51AF5F39AB0CEC3E5497CD9C900EBFEAECCCB5C7.8506521BFC350652163895D4C26DEE124209AA9E&key=ik0",
+ "extension": "mpd",
+ "abr_algorithm": "random"
+ }
+ ]
}
]
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 5d019e4c53..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,20 +16,51 @@
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;
+import com.google.android.exoplayer2.upstream.FileDataSourceFactory;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.TransferListener;
+import com.google.android.exoplayer2.upstream.cache.Cache;
+import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
+import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory;
+import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;
+import com.google.android.exoplayer2.upstream.cache.SimpleCache;
import com.google.android.exoplayer2.util.Util;
+import java.io.File;
/**
* Placeholder application to facilitate overriding Application methods for debugging and testing.
*/
public class DemoApplication extends Application {
+ 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() {
super.onCreate();
@@ -38,7 +69,9 @@ public class DemoApplication extends Application {
/** Returns a {@link DataSource.Factory}. */
public DataSource.Factory buildDataSourceFactory(TransferListener super DataSource> listener) {
- return new DefaultDataSourceFactory(this, listener, buildHttpDataSourceFactory(listener));
+ DefaultDataSourceFactory upstreamFactory =
+ new DefaultDataSourceFactory(this, listener, buildHttpDataSourceFactory(listener));
+ return buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache());
}
/** Returns a {@link HttpDataSource.Factory}. */
@@ -47,8 +80,69 @@ public class DemoApplication extends Application {
return new DefaultHttpDataSourceFactory(userAgent, listener);
}
+ /** Returns whether extension renderers should be used. */
public boolean useExtensionRenderers() {
- return BuildConfig.FLAVOR.equals("withExtensions");
+ return "withExtensions".equals(BuildConfig.FLAVOR);
}
+ 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,
+ CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR,
+ /* 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 058133895e..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.support.annotation.NonNull;
import android.support.annotation.Nullable;
-import android.text.TextUtils;
+import android.util.Pair;
import android.view.KeyEvent;
import android.view.View;
import android.view.View.OnClickListener;
@@ -42,43 +42,52 @@ import com.google.android.exoplayer2.PlaybackPreparer;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
-import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
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;
import com.google.android.exoplayer2.source.MediaSource;
-import com.google.android.exoplayer2.source.MediaSourceEventListener;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.ads.AdsLoader;
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
import com.google.android.exoplayer2.source.dash.DashMediaSource;
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
+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.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.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;
+import com.google.android.exoplayer2.trackselection.RandomTrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelection;
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.Util;
import java.lang.reflect.Constructor;
import java.net.CookieHandler;
import java.net.CookieManager;
import java.net.CookiePolicy;
+import java.util.List;
import java.util.UUID;
/** An activity that plays media using {@link SimpleExoPlayer}. */
@@ -86,10 +95,10 @@ public class PlayerActivity extends Activity
implements OnClickListener, PlaybackPreparer, PlayerControlView.VisibilityListener {
public static final String DRM_SCHEME_EXTRA = "drm_scheme";
- public static final String DRM_LICENSE_URL = "drm_license_url";
- public static final String DRM_KEY_REQUEST_PROPERTIES = "drm_key_request_properties";
- public static final String DRM_MULTI_SESSION = "drm_multi_session";
- public static final String PREFER_EXTENSION_DECODERS = "prefer_extension_decoders";
+ public static final String DRM_LICENSE_URL_EXTRA = "drm_license_url";
+ public static final String DRM_KEY_REQUEST_PROPERTIES_EXTRA = "drm_key_request_properties";
+ public static final String DRM_MULTI_SESSION_EXTRA = "drm_multi_session";
+ public static final String PREFER_EXTENSION_DECODERS_EXTRA = "prefer_extension_decoders";
public static final String ACTION_VIEW = "com.google.android.exoplayer.demo.action.VIEW";
public static final String EXTENSION_EXTRA = "extension";
@@ -98,11 +107,22 @@ public class PlayerActivity extends Activity
"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 AD_TAG_URI_EXTRA = "ad_tag_uri";
- // For backwards compatibility.
+ public static final String ABR_ALGORITHM_EXTRA = "abr_algorithm";
+ private static final String ABR_ALGORITHM_DEFAULT = "default";
+ private static final String ABR_ALGORITHM_RANDOM = "random";
+
+ // 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 {
@@ -110,23 +130,21 @@ public class PlayerActivity extends Activity
DEFAULT_COOKIE_MANAGER.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
}
- private Handler mainHandler;
- private EventLogger eventLogger;
private PlayerView playerView;
private LinearLayout debugRootView;
private TextView debugTextView;
private DataSource.Factory mediaDataSourceFactory;
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.
@@ -139,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);
}
@@ -155,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);
}
@@ -207,7 +232,12 @@ public class PlayerActivity extends Activity
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
- if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ 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);
@@ -215,6 +245,16 @@ public class PlayerActivity extends Activity
}
}
+ @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
@Override
@@ -230,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();
}
}
}
@@ -253,21 +304,40 @@ public class PlayerActivity extends Activity
// Internal methods
private void initializePlayer() {
- Intent intent = getIntent();
- boolean needNewPlayer = player == null;
- if (needNewPlayer) {
- TrackSelection.Factory adaptiveTrackSelectionFactory =
- new AdaptiveTrackSelection.Factory(BANDWIDTH_METER);
- trackSelector = new DefaultTrackSelector(adaptiveTrackSelectionFactory);
- trackSelectionHelper = new TrackSelectionHelper(trackSelector, adaptiveTrackSelectionFactory);
- lastSeenTrackGroupArray = null;
- eventLogger = new EventLogger(trackSelector);
+ if (player == null) {
+ Intent intent = getIntent();
+ String action = intent.getAction();
+ Uri[] uris;
+ String[] extensions;
+ if (ACTION_VIEW.equals(action)) {
+ uris = new Uri[] {intent.getData()};
+ extensions = new String[] {intent.getStringExtra(EXTENSION_EXTRA)};
+ } else if (ACTION_VIEW_LIST.equals(action)) {
+ String[] uriStrings = intent.getStringArrayExtra(URI_LIST_EXTRA);
+ uris = new Uri[uriStrings.length];
+ for (int i = 0; i < uriStrings.length; i++) {
+ uris[i] = Uri.parse(uriStrings[i]);
+ }
+ extensions = intent.getStringArrayExtra(EXTENSION_LIST_EXTRA);
+ if (extensions == null) {
+ extensions = new String[uriStrings.length];
+ }
+ } else {
+ showToast(getString(R.string.unexpected_intent_action, action));
+ finish();
+ return;
+ }
+ if (Util.maybeRequestReadExternalStoragePermission(this, uris)) {
+ // The player will be reinitialized if the permission is granted.
+ return;
+ }
- DrmSessionManager drmSessionManager = null;
+ DefaultDrmSessionManager drmSessionManager = null;
if (intent.hasExtra(DRM_SCHEME_EXTRA) || intent.hasExtra(DRM_SCHEME_UUID_EXTRA)) {
- String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL);
- String[] keyRequestPropertiesArray = intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES);
- boolean multiSession = intent.getBooleanExtra(DRM_MULTI_SESSION, false);
+ String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL_EXTRA);
+ String[] keyRequestPropertiesArray =
+ intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA);
+ boolean multiSession = intent.getBooleanExtra(DRM_MULTI_SESSION_EXTRA, false);
int errorStringId = R.string.error_drm_unknown;
if (Util.SDK_INT < 18) {
errorStringId = R.string.error_drm_not_supported;
@@ -290,154 +360,168 @@ public class PlayerActivity extends Activity
}
if (drmSessionManager == null) {
showToast(errorStringId);
+ finish();
return;
}
}
- boolean preferExtensionDecoders = intent.getBooleanExtra(PREFER_EXTENSION_DECODERS, false);
+ TrackSelection.Factory trackSelectionFactory;
+ String abrAlgorithm = intent.getStringExtra(ABR_ALGORITHM_EXTRA);
+ if (abrAlgorithm == null || ABR_ALGORITHM_DEFAULT.equals(abrAlgorithm)) {
+ trackSelectionFactory = new AdaptiveTrackSelection.Factory(BANDWIDTH_METER);
+ } else if (ABR_ALGORITHM_RANDOM.equals(abrAlgorithm)) {
+ trackSelectionFactory = new RandomTrackSelection.Factory();
+ } else {
+ showToast(R.string.error_unrecognized_abr_algorithm);
+ finish();
+ return;
+ }
+
+ boolean preferExtensionDecoders =
+ intent.getBooleanExtra(PREFER_EXTENSION_DECODERS_EXTRA, false);
@DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode =
((DemoApplication) getApplication()).useExtensionRenderers()
? (preferExtensionDecoders ? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF;
- DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(this,
- drmSessionManager, extensionRendererMode);
+ DefaultRenderersFactory renderersFactory =
+ new DefaultRenderersFactory(this, extensionRendererMode);
- player = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector);
+ trackSelector = new DefaultTrackSelector(trackSelectionFactory);
+ trackSelector.setParameters(trackSelectorParameters);
+ lastSeenTrackGroupArray = null;
+
+ player =
+ ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector, drmSessionManager);
player.addListener(new PlayerEventListener());
- player.addListener(eventLogger);
- player.addMetadataOutput(eventLogger);
- player.addAudioDebugListener(eventLogger);
- player.addVideoDebugListener(eventLogger);
- player.setPlayWhenReady(shouldAutoPlay);
-
+ player.setPlayWhenReady(startAutoPlay);
+ player.addAnalyticsListener(new EventLogger(trackSelector));
playerView.setPlayer(player);
playerView.setPlaybackPreparer(this);
debugViewHelper = new DebugTextViewHelper(player, debugTextView);
debugViewHelper.start();
- }
- String action = intent.getAction();
- Uri[] uris;
- String[] extensions;
- if (ACTION_VIEW.equals(action)) {
- uris = new Uri[]{intent.getData()};
- extensions = new String[]{intent.getStringExtra(EXTENSION_EXTRA)};
- } else if (ACTION_VIEW_LIST.equals(action)) {
- String[] uriStrings = intent.getStringArrayExtra(URI_LIST_EXTRA);
- uris = new Uri[uriStrings.length];
- for (int i = 0; i < uriStrings.length; i++) {
- uris[i] = Uri.parse(uriStrings[i]);
+
+ MediaSource[] mediaSources = new MediaSource[uris.length];
+ for (int i = 0; i < uris.length; i++) {
+ mediaSources[i] = buildMediaSource(uris[i], extensions[i]);
}
- extensions = intent.getStringArrayExtra(EXTENSION_LIST_EXTRA);
- if (extensions == null) {
- extensions = new String[uriStrings.length];
- }
- } else {
- showToast(getString(R.string.unexpected_intent_action, action));
- return;
- }
- if (Util.maybeRequestReadExternalStoragePermission(this, uris)) {
- // The player will be reinitialized if the permission is granted.
- return;
- }
- MediaSource[] mediaSources = new MediaSource[uris.length];
- for (int i = 0; i < uris.length; i++) {
- mediaSources[i] = buildMediaSource(uris[i], extensions[i], mainHandler, eventLogger);
- }
- MediaSource mediaSource = mediaSources.length == 1 ? mediaSources[0]
- : new ConcatenatingMediaSource(mediaSources);
- String adTagUriString = intent.getStringExtra(AD_TAG_URI_EXTRA);
- if (adTagUriString != null) {
- Uri adTagUri = Uri.parse(adTagUriString);
- if (!adTagUri.equals(loadedAdTagUri)) {
- releaseAdsLoader();
- loadedAdTagUri = adTagUri;
- }
- MediaSource adsMediaSource = createAdsMediaSource(mediaSource, Uri.parse(adTagUriString));
- if (adsMediaSource != null) {
- mediaSource = adsMediaSource;
+ mediaSource =
+ mediaSources.length == 1 ? mediaSources[0] : new ConcatenatingMediaSource(mediaSources);
+ String adTagUriString = intent.getStringExtra(AD_TAG_URI_EXTRA);
+ if (adTagUriString != null) {
+ Uri adTagUri = Uri.parse(adTagUriString);
+ if (!adTagUri.equals(loadedAdTagUri)) {
+ releaseAdsLoader();
+ loadedAdTagUri = adTagUri;
+ }
+ MediaSource adsMediaSource = createAdsMediaSource(mediaSource, Uri.parse(adTagUriString));
+ if (adsMediaSource != null) {
+ mediaSource = adsMediaSource;
+ } else {
+ showToast(R.string.ima_not_loaded);
+ }
} else {
- showToast(R.string.ima_not_loaded);
+ releaseAdsLoader();
}
- } else {
- releaseAdsLoader();
}
- 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,
- String overrideExtension,
- @Nullable Handler handler,
- @Nullable MediaSourceEventListener listener) {
- @ContentType int type = TextUtils.isEmpty(overrideExtension) ? Util.inferContentType(uri)
- : Util.inferContentType("." + overrideExtension);
+ private MediaSource buildMediaSource(Uri uri) {
+ return buildMediaSource(uri, null);
+ }
+
+ @SuppressWarnings("unchecked")
+ private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) {
+ @ContentType int type = Util.inferContentType(uri, overrideExtension);
switch (type) {
case C.TYPE_DASH:
return new DashMediaSource.Factory(
new DefaultDashChunkSource.Factory(mediaDataSourceFactory),
buildDataSourceFactory(false))
- .createMediaSource(uri, handler, listener);
+ .setManifestParser(
+ new FilteringManifestParser<>(
+ new DashManifestParser(), (List) getOfflineStreamKeys(uri)))
+ .createMediaSource(uri);
case C.TYPE_SS:
return new SsMediaSource.Factory(
new DefaultSsChunkSource.Factory(mediaDataSourceFactory),
buildDataSourceFactory(false))
- .createMediaSource(uri, handler, listener);
+ .setManifestParser(
+ new FilteringManifestParser<>(
+ new SsManifestParser(), (List) getOfflineStreamKeys(uri)))
+ .createMediaSource(uri);
case C.TYPE_HLS:
return new HlsMediaSource.Factory(mediaDataSourceFactory)
- .createMediaSource(uri, handler, listener);
+ .setPlaylistParser(
+ new FilteringManifestParser<>(
+ new HlsPlaylistParser(), (List) getOfflineStreamKeys(uri)))
+ .createMediaSource(uri);
case C.TYPE_OTHER:
- return new ExtractorMediaSource.Factory(mediaDataSourceFactory)
- .createMediaSource(uri, handler, listener);
+ return new ExtractorMediaSource.Factory(mediaDataSourceFactory).createMediaSource(uri);
default: {
throw new IllegalStateException("Unsupported type: " + type);
}
}
}
- private DrmSessionManager buildDrmSessionManagerV18(UUID uuid,
- String licenseUrl, String[] keyRequestPropertiesArray, boolean multiSession)
+ 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]);
}
}
- return new DefaultDrmSessionManager<>(uuid, FrameworkMediaDrm.newInstance(uuid), drmCallback,
- null, mainHandler, eventLogger, multiSession);
+ 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;
- eventLogger = 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;
}
/**
@@ -452,18 +536,6 @@ 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) {
// Load the extension source using reflection so the demo app doesn't have to depend on it.
@@ -486,10 +558,8 @@ public class PlayerActivity extends Activity
AdsMediaSource.MediaSourceFactory adMediaSourceFactory =
new AdsMediaSource.MediaSourceFactory() {
@Override
- public MediaSource createMediaSource(
- Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) {
- return PlayerActivity.this.buildMediaSource(
- uri, /* overrideExtension= */ null, handler, listener);
+ public MediaSource createMediaSource(Uri uri) {
+ return PlayerActivity.this.buildMediaSource(uri);
}
@Override
@@ -497,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;
@@ -529,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;
@@ -593,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();
}
@@ -647,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);
}
@@ -659,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 3895ad8e84..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,20 +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;
-import java.util.UUID;
-/**
- * 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;
@@ -80,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) {
@@ -89,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> {
@@ -176,15 +233,16 @@ 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;
- UUID drmUuid = null;
+ String drmScheme = null;
String drmLicenseUrl = null;
String[] drmKeyRequestProperties = null;
boolean drmMultiSession = false;
boolean preferExtensionDecoders = false;
ArrayList playlistSamples = null;
String adTagUri = null;
+ String abrAlgorithm = null;
reader.beginObject();
while (reader.hasNext()) {
@@ -194,16 +252,14 @@ 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();
break;
case "drm_scheme":
Assertions.checkState(!insidePlaylist, "Invalid attribute on nested item: drm_scheme");
- String drmScheme = reader.nextString();
- drmUuid = Util.getDrmUuid(drmScheme);
- Assertions.checkState(drmUuid != null, "Invalid drm_scheme: " + drmScheme);
+ drmScheme = reader.nextString();
break;
case "drm_license_url":
Assertions.checkState(!insidePlaylist,
@@ -242,21 +298,28 @@ public class SampleChooserActivity extends Activity {
case "ad_tag_uri":
adTagUri = reader.nextString();
break;
+ case "abr_algorithm":
+ Assertions.checkState(
+ !insidePlaylist, "Invalid attribute on nested item: abr_algorithm");
+ abrAlgorithm = reader.nextString();
+ break;
default:
throw new ParserException("Unsupported attribute name: " + name);
}
}
reader.endObject();
- DrmInfo drmInfo = drmUuid == null ? null : new DrmInfo(drmUuid, drmLicenseUrl,
- drmKeyRequestProperties, drmMultiSession);
+ DrmInfo drmInfo =
+ drmScheme == null
+ ? null
+ : new DrmInfo(drmScheme, drmLicenseUrl, drmKeyRequestProperties, drmMultiSession);
if (playlistSamples != null) {
UriSample[] playlistSamplesArray = playlistSamples.toArray(
new UriSample[playlistSamples.size()]);
- return new PlaylistSample(sampleName, preferExtensionDecoders, drmInfo,
- playlistSamplesArray);
+ return new PlaylistSample(
+ sampleName, preferExtensionDecoders, abrAlgorithm, drmInfo, playlistSamplesArray);
} else {
- return new UriSample(sampleName, preferExtensionDecoders, drmInfo, uri, extension,
- adTagUri);
+ return new UriSample(
+ sampleName, preferExtensionDecoders, abrAlgorithm, drmInfo, uri, extension, adTagUri);
}
}
@@ -273,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
@@ -298,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;
}
@@ -325,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;
@@ -347,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 {
@@ -362,14 +450,17 @@ public class SampleChooserActivity extends Activity {
}
private static final class DrmInfo {
- public final UUID drmSchemeUuid;
+ public final String drmScheme;
public final String drmLicenseUrl;
public final String[] drmKeyRequestProperties;
public final boolean drmMultiSession;
- public DrmInfo(UUID drmSchemeUuid, String drmLicenseUrl,
- String[] drmKeyRequestProperties, boolean drmMultiSession) {
- this.drmSchemeUuid = drmSchemeUuid;
+ public DrmInfo(
+ String drmScheme,
+ String drmLicenseUrl,
+ String[] drmKeyRequestProperties,
+ boolean drmMultiSession) {
+ this.drmScheme = drmScheme;
this.drmLicenseUrl = drmLicenseUrl;
this.drmKeyRequestProperties = drmKeyRequestProperties;
this.drmMultiSession = drmMultiSession;
@@ -377,31 +468,34 @@ public class SampleChooserActivity extends Activity {
public void updateIntent(Intent intent) {
Assertions.checkNotNull(intent);
- intent.putExtra(PlayerActivity.DRM_SCHEME_EXTRA, drmSchemeUuid.toString());
- intent.putExtra(PlayerActivity.DRM_LICENSE_URL, drmLicenseUrl);
- intent.putExtra(PlayerActivity.DRM_KEY_REQUEST_PROPERTIES, drmKeyRequestProperties);
- intent.putExtra(PlayerActivity.DRM_MULTI_SESSION, drmMultiSession);
+ intent.putExtra(PlayerActivity.DRM_SCHEME_EXTRA, drmScheme);
+ intent.putExtra(PlayerActivity.DRM_LICENSE_URL_EXTRA, drmLicenseUrl);
+ intent.putExtra(PlayerActivity.DRM_KEY_REQUEST_PROPERTIES_EXTRA, drmKeyRequestProperties);
+ intent.putExtra(PlayerActivity.DRM_MULTI_SESSION_EXTRA, drmMultiSession);
}
}
private abstract static class Sample {
public final String name;
public final boolean preferExtensionDecoders;
+ public final String abrAlgorithm;
public final DrmInfo drmInfo;
- public Sample(String name, boolean preferExtensionDecoders, DrmInfo drmInfo) {
+ public Sample(
+ String name, boolean preferExtensionDecoders, String abrAlgorithm, DrmInfo drmInfo) {
this.name = name;
this.preferExtensionDecoders = preferExtensionDecoders;
+ this.abrAlgorithm = abrAlgorithm;
this.drmInfo = drmInfo;
}
public Intent buildIntent(Context context) {
Intent intent = new Intent(context, PlayerActivity.class);
- intent.putExtra(PlayerActivity.PREFER_EXTENSION_DECODERS, preferExtensionDecoders);
+ intent.putExtra(PlayerActivity.PREFER_EXTENSION_DECODERS_EXTRA, preferExtensionDecoders);
+ intent.putExtra(PlayerActivity.ABR_ALGORITHM_EXTRA, abrAlgorithm);
if (drmInfo != null) {
drmInfo.updateIntent(intent);
}
-
return intent;
}
@@ -409,13 +503,19 @@ 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;
- public UriSample(String name, boolean preferExtensionDecoders, DrmInfo drmInfo, String uri,
- String extension, String adTagUri) {
- super(name, preferExtensionDecoders, drmInfo);
+ public UriSample(
+ String name,
+ boolean preferExtensionDecoders,
+ String abrAlgorithm,
+ DrmInfo drmInfo,
+ Uri uri,
+ String extension,
+ String adTagUri) {
+ super(name, preferExtensionDecoders, abrAlgorithm, drmInfo);
this.uri = uri;
this.extension = extension;
this.adTagUri = adTagUri;
@@ -424,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);
@@ -436,9 +536,13 @@ public class SampleChooserActivity extends Activity {
public final UriSample[] children;
- public PlaylistSample(String name, boolean preferExtensionDecoders, DrmInfo drmInfo,
+ public PlaylistSample(
+ String name,
+ boolean preferExtensionDecoders,
+ String abrAlgorithm,
+ DrmInfo drmInfo,
UriSample... children) {
- super(name, preferExtensionDecoders, drmInfo);
+ super(name, preferExtensionDecoders, abrAlgorithm, drmInfo);
this.children = children;
}
@@ -447,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 e033b91eef..0000000000
--- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java
+++ /dev/null
@@ -1,290 +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.FixedTrackSelection;
-import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
-import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
-import com.google.android.exoplayer2.trackselection.MappingTrackSelector.SelectionOverride;
-import com.google.android.exoplayer2.trackselection.RandomTrackSelection;
-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 static final TrackSelection.Factory FIXED_FACTORY = new FixedTrackSelection.Factory();
- private static final TrackSelection.Factory RANDOM_FACTORY = new RandomTrackSelection.Factory();
-
- private final MappingTrackSelector selector;
- private final TrackSelection.Factory adaptiveTrackSelectionFactory;
-
- 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 enableRandomAdaptationView;
- private CheckedTextView[][] trackViews;
-
- /**
- * @param selector The track selector.
- * @param adaptiveTrackSelectionFactory A factory for adaptive {@link TrackSelection}s, or null
- * if the selection helper should not support adaptive tracks.
- */
- public TrackSelectionHelper(MappingTrackSelector selector,
- TrackSelection.Factory adaptiveTrackSelectionFactory) {
- this.selector = selector;
- this.adaptiveTrackSelectionFactory = adaptiveTrackSelectionFactory;
- }
-
- /**
- * 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] = adaptiveTrackSelectionFactory != null
- && 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.
- boolean haveAdaptiveTracks = false;
- trackViews = new CheckedTextView[trackGroups.length][];
- for (int groupIndex = 0; groupIndex < trackGroups.length; groupIndex++) {
- TrackGroup group = trackGroups.get(groupIndex);
- boolean groupIsAdaptive = trackGroupsAdaptive[groupIndex];
- haveAdaptiveTracks |= groupIsAdaptive;
- 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);
- }
- }
-
- if (haveAdaptiveTracks) {
- // View for using random adaptation.
- enableRandomAdaptationView = (CheckedTextView) inflater.inflate(
- android.R.layout.simple_list_item_multiple_choice, root, false);
- enableRandomAdaptationView.setBackgroundResource(selectableItemBackgroundResourceId);
- enableRandomAdaptationView.setText(R.string.enable_random_adaptation);
- enableRandomAdaptationView.setOnClickListener(this);
- root.addView(inflater.inflate(R.layout.list_divider, root, false));
- root.addView(enableRandomAdaptationView);
- }
-
- 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));
- }
- }
- if (enableRandomAdaptationView != null) {
- boolean enableView = !isDisabled && override != null && override.length > 1;
- enableRandomAdaptationView.setEnabled(enableView);
- enableRandomAdaptationView.setFocusable(enableView);
- if (enableView) {
- enableRandomAdaptationView.setChecked(!isDisabled
- && override.factory instanceof RandomTrackSelection.Factory);
- }
- }
- }
-
- // 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 if (view == enableRandomAdaptationView) {
- setOverride(override.groupIndex, override.tracks, !enableRandomAdaptationView.isChecked());
- } 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) {
- override = new SelectionOverride(FIXED_FACTORY, 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),
- enableRandomAdaptationView.isChecked());
- }
- } else {
- // Add the track to the override.
- setOverride(groupIndex, getTracksAdding(override, trackIndex),
- enableRandomAdaptationView.isChecked());
- }
- }
- }
- // Update the views with the new state.
- updateViews();
- }
-
- private void setOverride(int group, int[] tracks, boolean enableRandomAdaptation) {
- TrackSelection.Factory factory = tracks.length == 1 ? FIXED_FACTORY
- : (enableRandomAdaptation ? RANDOM_FACTORY : adaptiveTrackSelectionFactory);
- override = new SelectionOverride(factory, 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/proguard-rules.txt b/demos/main/src/main/proguard-rules.txt
deleted file mode 100644
index cd201892ab..0000000000
--- a/demos/main/src/main/proguard-rules.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-# Proguard rules specific to the main demo app.
-
-# Constructor accessed via reflection in PlayerActivity
--dontnote com.google.android.exoplayer2.ext.ima.ImaAdsLoader
--keepclassmembers class com.google.android.exoplayer2.ext.ima.ImaAdsLoader {
- (android.content.Context, android.net.Uri);
-}
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 43b17052fb..eb260e6ffc 100644
--- a/demos/main/src/main/res/values/strings.xml
+++ b/demos/main/src/main/res/values/strings.xml
@@ -17,19 +17,11 @@
ExoPlayer
- Video
-
- Audio
-
- Text
-
- Disabled
-
- Default
-
Unexpected intent action: %1$s
- Enable random adaptation
+ 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/build.gradle b/extensions/cast/build.gradle
index d7e99573cb..ded92000d3 100644
--- a/extensions/cast/build.gradle
+++ b/extensions/cast/build.gradle
@@ -30,9 +30,9 @@ dependencies {
// com.android.support:support-v4, com.android.support:appcompat-v7 and
// com.android.support:mediarouter-v7 to be used. Else older versions are
// used, for example:
- // com.google.android.gms:play-services-cast-framework:11.4.2
- // |-- com.google.android.gms:play-services-basement:11.4.2
- // |-- com.android.support:support-v4:25.2.0
+ // com.google.android.gms:play-services-cast-framework:12.0.0
+ // |-- com.google.android.gms:play-services-basement:12.0.0
+ // |-- com.android.support:support-v4:26.1.0
api 'com.android.support:support-v4:' + supportLibraryVersion
api 'com.android.support:appcompat-v7:' + supportLibraryVersion
api 'com.android.support:mediarouter-v7:' + supportLibraryVersion
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 50c883c3f6..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) {
@@ -481,6 +487,14 @@ public final class CastPlayer implements Player {
: currentTimeline.getPreviousWindowIndex(getCurrentWindowIndex(), repeatMode, false);
}
+ @Override
+ public @Nullable Object getCurrentTag() {
+ int windowIndex = getCurrentWindowIndex();
+ return windowIndex > currentTimeline.getWindowCount()
+ ? null
+ : currentTimeline.getWindow(windowIndex, window, /* setTag= */ true).tag;
+ }
+
// TODO: Fill the cast timeline information with ProgressListener's duration updates.
// See [Internal: b/65152553].
@Override
diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java
index a0be844439..24d815bae2 100644
--- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java
+++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java
@@ -73,12 +73,22 @@ import java.util.Map;
}
@Override
- public Window getWindow(int windowIndex, Window window, boolean setIds,
- long defaultPositionProjectionUs) {
+ public Window getWindow(
+ int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) {
long durationUs = durationsUs[windowIndex];
boolean isDynamic = durationUs == C.TIME_UNSET;
- return window.set(ids[windowIndex], C.TIME_UNSET, C.TIME_UNSET, !isDynamic, isDynamic,
- defaultPositionsUs[windowIndex], durationUs, windowIndex, windowIndex, 0);
+ Object tag = setTag ? ids[windowIndex] : null;
+ return window.set(
+ tag,
+ /* presentationStartTimeMs= */ C.TIME_UNSET,
+ /* windowStartTimeMs= */ C.TIME_UNSET,
+ /* isSeekable= */ !isDynamic,
+ isDynamic,
+ defaultPositionsUs[windowIndex],
+ durationUs,
+ /* firstPeriodIndex= */ windowIndex,
+ /* lastPeriodIndex= */ windowIndex,
+ /* positionInFirstPeriodUs= */ 0);
}
@Override
diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java
index f17c39bdbf..d2154eec1b 100644
--- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java
+++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java
@@ -89,7 +89,7 @@ import com.google.android.gms.cast.MediaTrack;
case CastStatusCodes.UNKNOWN_ERROR:
return "An unknown, unexpected error has occurred.";
default:
- return "Unknown: " + statusCode;
+ return CastStatusCodes.getStatusCodeString(statusCode);
}
}
diff --git a/extensions/cast/src/test/AndroidManifest.xml b/extensions/cast/src/test/AndroidManifest.xml
index 3f34bbb1f5..aea8bda663 100644
--- a/extensions/cast/src/test/AndroidManifest.xml
+++ b/extensions/cast/src/test/AndroidManifest.xml
@@ -14,9 +14,4 @@
limitations under the License.
-->
-
-
-
-
-
+
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/cronet/src/test/AndroidManifest.xml b/extensions/cronet/src/test/AndroidManifest.xml
index a1512ae605..82cffe17c2 100644
--- a/extensions/cronet/src/test/AndroidManifest.xml
+++ b/extensions/cronet/src/test/AndroidManifest.xml
@@ -14,9 +14,4 @@
limitations under the License.
-->
-
-
-
-
-
+
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 f617064ce5..609953130b 100644
--- a/extensions/flac/build.gradle
+++ b/extensions/flac/build.gradle
@@ -31,8 +31,10 @@ android {
}
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/androidTest/AndroidManifest.xml b/extensions/flac/src/androidTest/AndroidManifest.xml
index 38a6bfc927..4e3925d8e3 100644
--- a/extensions/flac/src/androidTest/AndroidManifest.xml
+++ b/extensions/flac/src/androidTest/AndroidManifest.xml
@@ -18,8 +18,6 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.google.android.exoplayer2.ext.flac.test">
-
-
diff --git a/extensions/flac/src/androidTest/assets/bear.flac.0.dump b/extensions/flac/src/androidTest/assets/bear.flac.0.dump
index ad88981718..71359322b0 100644
--- a/extensions/flac/src/androidTest/assets/bear.flac.0.dump
+++ b/extensions/flac/src/androidTest/assets/bear.flac.0.dump
@@ -13,13 +13,13 @@ track 0:
width = -1
height = -1
frameRate = -1.0
- rotationDegrees = -1
- pixelWidthHeightRatio = -1.0
+ rotationDegrees = 0
+ pixelWidthHeightRatio = 1.0
channelCount = 2
sampleRate = 48000
pcmEncoding = 2
- encoderDelay = -1
- encoderPadding = -1
+ encoderDelay = 0
+ encoderPadding = 0
subsampleOffsetUs = 9223372036854775807
selectionFlags = 0
language = null
diff --git a/extensions/flac/src/androidTest/assets/bear.flac.1.dump b/extensions/flac/src/androidTest/assets/bear.flac.1.dump
index 22f30e9db2..820b9eed10 100644
--- a/extensions/flac/src/androidTest/assets/bear.flac.1.dump
+++ b/extensions/flac/src/androidTest/assets/bear.flac.1.dump
@@ -13,13 +13,13 @@ track 0:
width = -1
height = -1
frameRate = -1.0
- rotationDegrees = -1
- pixelWidthHeightRatio = -1.0
+ rotationDegrees = 0
+ pixelWidthHeightRatio = 1.0
channelCount = 2
sampleRate = 48000
pcmEncoding = 2
- encoderDelay = -1
- encoderPadding = -1
+ encoderDelay = 0
+ encoderPadding = 0
subsampleOffsetUs = 9223372036854775807
selectionFlags = 0
language = null
diff --git a/extensions/flac/src/androidTest/assets/bear.flac.2.dump b/extensions/flac/src/androidTest/assets/bear.flac.2.dump
index c52a74cbfb..c2d58347eb 100644
--- a/extensions/flac/src/androidTest/assets/bear.flac.2.dump
+++ b/extensions/flac/src/androidTest/assets/bear.flac.2.dump
@@ -13,13 +13,13 @@ track 0:
width = -1
height = -1
frameRate = -1.0
- rotationDegrees = -1
- pixelWidthHeightRatio = -1.0
+ rotationDegrees = 0
+ pixelWidthHeightRatio = 1.0
channelCount = 2
sampleRate = 48000
pcmEncoding = 2
- encoderDelay = -1
- encoderPadding = -1
+ encoderDelay = 0
+ encoderPadding = 0
subsampleOffsetUs = 9223372036854775807
selectionFlags = 0
language = null
diff --git a/extensions/flac/src/androidTest/assets/bear.flac.3.dump b/extensions/flac/src/androidTest/assets/bear.flac.3.dump
index 760f369597..8c1115f1ec 100644
--- a/extensions/flac/src/androidTest/assets/bear.flac.3.dump
+++ b/extensions/flac/src/androidTest/assets/bear.flac.3.dump
@@ -13,13 +13,13 @@ track 0:
width = -1
height = -1
frameRate = -1.0
- rotationDegrees = -1
- pixelWidthHeightRatio = -1.0
+ rotationDegrees = 0
+ pixelWidthHeightRatio = 1.0
channelCount = 2
sampleRate = 48000
pcmEncoding = 2
- encoderDelay = -1
- encoderPadding = -1
+ encoderDelay = 0
+ encoderPadding = 0
subsampleOffsetUs = 9223372036854775807
selectionFlags = 0
language = null
diff --git a/extensions/flac/src/androidTest/assets/bear_with_id3.flac b/extensions/flac/src/androidTest/assets/bear_with_id3.flac
new file mode 100644
index 0000000000..fc945f14ad
Binary files /dev/null and b/extensions/flac/src/androidTest/assets/bear_with_id3.flac differ
diff --git a/extensions/flac/src/androidTest/assets/bear_with_id3.flac.0.dump b/extensions/flac/src/androidTest/assets/bear_with_id3.flac.0.dump
new file mode 100644
index 0000000000..d8903fcade
--- /dev/null
+++ b/extensions/flac/src/androidTest/assets/bear_with_id3.flac.0.dump
@@ -0,0 +1,162 @@
+seekMap:
+ isSeekable = true
+ duration = 2741000
+ getPosition(0) = [[timeUs=0, position=55284]]
+numberOfTracks = 1
+track 0:
+ format:
+ bitrate = 768000
+ id = null
+ containerMimeType = null
+ sampleMimeType = audio/raw
+ maxInputSize = 16384
+ width = -1
+ height = -1
+ frameRate = -1.0
+ rotationDegrees = 0
+ pixelWidthHeightRatio = 1.0
+ channelCount = 2
+ sampleRate = 48000
+ pcmEncoding = 2
+ encoderDelay = 0
+ encoderPadding = 0
+ subsampleOffsetUs = 9223372036854775807
+ selectionFlags = 0
+ language = null
+ drmInitData = -
+ initializationData:
+ total output bytes = 526272
+ sample count = 33
+ sample 0:
+ time = 0
+ flags = 1
+ data = length 16384, hash 61D2C5C2
+ sample 1:
+ time = 85333
+ flags = 1
+ data = length 16384, hash E6D7F214
+ sample 2:
+ time = 170666
+ flags = 1
+ data = length 16384, hash 59BF0D5D
+ sample 3:
+ time = 256000
+ flags = 1
+ data = length 16384, hash 3625F468
+ sample 4:
+ time = 341333
+ flags = 1
+ data = length 16384, hash F66A323
+ sample 5:
+ time = 426666
+ flags = 1
+ data = length 16384, hash CDBAE629
+ sample 6:
+ time = 512000
+ flags = 1
+ data = length 16384, hash 536F3A91
+ sample 7:
+ time = 597333
+ flags = 1
+ data = length 16384, hash D4F35C9C
+ sample 8:
+ time = 682666
+ flags = 1
+ data = length 16384, hash EE04CEBF
+ sample 9:
+ time = 768000
+ flags = 1
+ data = length 16384, hash 647E2A67
+ sample 10:
+ time = 853333
+ flags = 1
+ data = length 16384, hash 31583F2C
+ sample 11:
+ time = 938666
+ flags = 1
+ data = length 16384, hash E433A93D
+ sample 12:
+ time = 1024000
+ flags = 1
+ data = length 16384, hash 5E1C7051
+ sample 13:
+ time = 1109333
+ flags = 1
+ data = length 16384, hash 43E6E358
+ sample 14:
+ time = 1194666
+ flags = 1
+ data = length 16384, hash 5DC1B256
+ sample 15:
+ time = 1280000
+ flags = 1
+ data = length 16384, hash 3D9D95CF
+ sample 16:
+ time = 1365333
+ flags = 1
+ data = length 16384, hash 2A5BD2C0
+ sample 17:
+ time = 1450666
+ flags = 1
+ data = length 16384, hash 93E25061
+ sample 18:
+ time = 1536000
+ flags = 1
+ data = length 16384, hash B81793D8
+ sample 19:
+ time = 1621333
+ flags = 1
+ data = length 16384, hash 1A3BD49F
+ sample 20:
+ time = 1706666
+ flags = 1
+ data = length 16384, hash FB672FF1
+ sample 21:
+ time = 1792000
+ flags = 1
+ data = length 16384, hash 48AB8B45
+ sample 22:
+ time = 1877333
+ flags = 1
+ data = length 16384, hash 13C9640A
+ sample 23:
+ time = 1962666
+ flags = 1
+ data = length 16384, hash 499E4A0B
+ sample 24:
+ time = 2048000
+ flags = 1
+ data = length 16384, hash F9A783E6
+ sample 25:
+ time = 2133333
+ flags = 1
+ data = length 16384, hash D2B77598
+ sample 26:
+ time = 2218666
+ flags = 1
+ data = length 16384, hash CE5B826C
+ sample 27:
+ time = 2304000
+ flags = 1
+ data = length 16384, hash E99EE956
+ sample 28:
+ time = 2389333
+ flags = 1
+ data = length 16384, hash F2DB1486
+ sample 29:
+ time = 2474666
+ flags = 1
+ data = length 16384, hash 1636EAB
+ sample 30:
+ time = 2560000
+ flags = 1
+ data = length 16384, hash 23457C08
+ sample 31:
+ time = 2645333
+ flags = 1
+ data = length 16384, hash 30EB8381
+ sample 32:
+ time = 2730666
+ flags = 1
+ data = length 1984, hash 59CFDE1B
+tracksEnded = true
diff --git a/extensions/flac/src/androidTest/assets/bear_with_id3.flac.1.dump b/extensions/flac/src/androidTest/assets/bear_with_id3.flac.1.dump
new file mode 100644
index 0000000000..100fdd1eaf
--- /dev/null
+++ b/extensions/flac/src/androidTest/assets/bear_with_id3.flac.1.dump
@@ -0,0 +1,122 @@
+seekMap:
+ isSeekable = true
+ duration = 2741000
+ getPosition(0) = [[timeUs=0, position=55284]]
+numberOfTracks = 1
+track 0:
+ format:
+ bitrate = 768000
+ id = null
+ containerMimeType = null
+ sampleMimeType = audio/raw
+ maxInputSize = 16384
+ width = -1
+ height = -1
+ frameRate = -1.0
+ rotationDegrees = 0
+ pixelWidthHeightRatio = 1.0
+ channelCount = 2
+ sampleRate = 48000
+ pcmEncoding = 2
+ encoderDelay = 0
+ encoderPadding = 0
+ subsampleOffsetUs = 9223372036854775807
+ selectionFlags = 0
+ language = null
+ drmInitData = -
+ initializationData:
+ total output bytes = 362432
+ sample count = 23
+ sample 0:
+ time = 853333
+ flags = 1
+ data = length 16384, hash 31583F2C
+ sample 1:
+ time = 938666
+ flags = 1
+ data = length 16384, hash E433A93D
+ sample 2:
+ time = 1024000
+ flags = 1
+ data = length 16384, hash 5E1C7051
+ sample 3:
+ time = 1109333
+ flags = 1
+ data = length 16384, hash 43E6E358
+ sample 4:
+ time = 1194666
+ flags = 1
+ data = length 16384, hash 5DC1B256
+ sample 5:
+ time = 1280000
+ flags = 1
+ data = length 16384, hash 3D9D95CF
+ sample 6:
+ time = 1365333
+ flags = 1
+ data = length 16384, hash 2A5BD2C0
+ sample 7:
+ time = 1450666
+ flags = 1
+ data = length 16384, hash 93E25061
+ sample 8:
+ time = 1536000
+ flags = 1
+ data = length 16384, hash B81793D8
+ sample 9:
+ time = 1621333
+ flags = 1
+ data = length 16384, hash 1A3BD49F
+ sample 10:
+ time = 1706666
+ flags = 1
+ data = length 16384, hash FB672FF1
+ sample 11:
+ time = 1792000
+ flags = 1
+ data = length 16384, hash 48AB8B45
+ sample 12:
+ time = 1877333
+ flags = 1
+ data = length 16384, hash 13C9640A
+ sample 13:
+ time = 1962666
+ flags = 1
+ data = length 16384, hash 499E4A0B
+ sample 14:
+ time = 2048000
+ flags = 1
+ data = length 16384, hash F9A783E6
+ sample 15:
+ time = 2133333
+ flags = 1
+ data = length 16384, hash D2B77598
+ sample 16:
+ time = 2218666
+ flags = 1
+ data = length 16384, hash CE5B826C
+ sample 17:
+ time = 2304000
+ flags = 1
+ data = length 16384, hash E99EE956
+ sample 18:
+ time = 2389333
+ flags = 1
+ data = length 16384, hash F2DB1486
+ sample 19:
+ time = 2474666
+ flags = 1
+ data = length 16384, hash 1636EAB
+ sample 20:
+ time = 2560000
+ flags = 1
+ data = length 16384, hash 23457C08
+ sample 21:
+ time = 2645333
+ flags = 1
+ data = length 16384, hash 30EB8381
+ sample 22:
+ time = 2730666
+ flags = 1
+ data = length 1984, hash 59CFDE1B
+tracksEnded = true
diff --git a/extensions/flac/src/androidTest/assets/bear_with_id3.flac.2.dump b/extensions/flac/src/androidTest/assets/bear_with_id3.flac.2.dump
new file mode 100644
index 0000000000..6c3cd731b3
--- /dev/null
+++ b/extensions/flac/src/androidTest/assets/bear_with_id3.flac.2.dump
@@ -0,0 +1,78 @@
+seekMap:
+ isSeekable = true
+ duration = 2741000
+ getPosition(0) = [[timeUs=0, position=55284]]
+numberOfTracks = 1
+track 0:
+ format:
+ bitrate = 768000
+ id = null
+ containerMimeType = null
+ sampleMimeType = audio/raw
+ maxInputSize = 16384
+ width = -1
+ height = -1
+ frameRate = -1.0
+ rotationDegrees = 0
+ pixelWidthHeightRatio = 1.0
+ channelCount = 2
+ sampleRate = 48000
+ pcmEncoding = 2
+ encoderDelay = 0
+ encoderPadding = 0
+ subsampleOffsetUs = 9223372036854775807
+ selectionFlags = 0
+ language = null
+ drmInitData = -
+ initializationData:
+ total output bytes = 182208
+ sample count = 12
+ sample 0:
+ time = 1792000
+ flags = 1
+ data = length 16384, hash 48AB8B45
+ sample 1:
+ time = 1877333
+ flags = 1
+ data = length 16384, hash 13C9640A
+ sample 2:
+ time = 1962666
+ flags = 1
+ data = length 16384, hash 499E4A0B
+ sample 3:
+ time = 2048000
+ flags = 1
+ data = length 16384, hash F9A783E6
+ sample 4:
+ time = 2133333
+ flags = 1
+ data = length 16384, hash D2B77598
+ sample 5:
+ time = 2218666
+ flags = 1
+ data = length 16384, hash CE5B826C
+ sample 6:
+ time = 2304000
+ flags = 1
+ data = length 16384, hash E99EE956
+ sample 7:
+ time = 2389333
+ flags = 1
+ data = length 16384, hash F2DB1486
+ sample 8:
+ time = 2474666
+ flags = 1
+ data = length 16384, hash 1636EAB
+ sample 9:
+ time = 2560000
+ flags = 1
+ data = length 16384, hash 23457C08
+ sample 10:
+ time = 2645333
+ flags = 1
+ data = length 16384, hash 30EB8381
+ sample 11:
+ time = 2730666
+ flags = 1
+ data = length 1984, hash 59CFDE1B
+tracksEnded = true
diff --git a/extensions/flac/src/androidTest/assets/bear_with_id3.flac.3.dump b/extensions/flac/src/androidTest/assets/bear_with_id3.flac.3.dump
new file mode 100644
index 0000000000..decf9c6af3
--- /dev/null
+++ b/extensions/flac/src/androidTest/assets/bear_with_id3.flac.3.dump
@@ -0,0 +1,38 @@
+seekMap:
+ isSeekable = true
+ duration = 2741000
+ getPosition(0) = [[timeUs=0, position=55284]]
+numberOfTracks = 1
+track 0:
+ format:
+ bitrate = 768000
+ id = null
+ containerMimeType = null
+ sampleMimeType = audio/raw
+ maxInputSize = 16384
+ width = -1
+ height = -1
+ frameRate = -1.0
+ rotationDegrees = 0
+ pixelWidthHeightRatio = 1.0
+ channelCount = 2
+ sampleRate = 48000
+ pcmEncoding = 2
+ encoderDelay = 0
+ encoderPadding = 0
+ subsampleOffsetUs = 9223372036854775807
+ selectionFlags = 0
+ language = null
+ drmInitData = -
+ initializationData:
+ total output bytes = 18368
+ sample count = 2
+ sample 0:
+ time = 2645333
+ flags = 1
+ data = length 16384, hash 30EB8381
+ sample 1:
+ time = 2730666
+ flags = 1
+ data = length 1984, hash 59CFDE1B
+tracksEnded = true
diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java
index c5f1f5c146..fc9bdac2ea 100644
--- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java
+++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java
@@ -33,7 +33,7 @@ public class FlacExtractorTest extends InstrumentationTestCase {
}
}
- public void testSample() throws Exception {
+ public void testExtractFlacSample() throws Exception {
ExtractorAsserts.assertBehavior(
new ExtractorFactory() {
@Override
@@ -44,4 +44,16 @@ public class FlacExtractorTest extends InstrumentationTestCase {
"bear.flac",
getInstrumentation().getContext());
}
+
+ public void testExtractFlacSampleWithId3Header() throws Exception {
+ ExtractorAsserts.assertBehavior(
+ new ExtractorFactory() {
+ @Override
+ public Extractor create() {
+ return new FlacExtractor();
+ }
+ },
+ "bear_with_id3.flac",
+ getInstrumentation().getContext());
+ }
}
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 6859b44877..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
@@ -17,20 +17,27 @@ package com.google.android.exoplayer2.ext.flac;
import static com.google.android.exoplayer2.util.Util.getPcmEncoding;
+import android.support.annotation.IntDef;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
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.Id3Peeker;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.SeekPoint;
import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.metadata.Metadata;
+import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
import com.google.android.exoplayer2.util.FlacStreamInfo;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray;
import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.nio.ByteBuffer;
import java.util.Arrays;
@@ -51,22 +58,56 @@ public final class FlacExtractor implements Extractor {
};
+ /** Flags controlling the behavior of the extractor. */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ flag = true,
+ value = {FLAG_DISABLE_ID3_METADATA}
+ )
+ public @interface Flags {}
+
+ /**
+ * Flag to disable parsing of ID3 metadata. Can be set to save memory if ID3 metadata is not
+ * required.
+ */
+ public static final int FLAG_DISABLE_ID3_METADATA = 1;
+
/**
* FLAC signature: first 4 is the signature word, second 4 is the sizeof STREAMINFO. 0x22 is the
* mandatory STREAMINFO.
*/
private static final byte[] FLAC_SIGNATURE = {'f', 'L', 'a', 'C', 0, 0, 0, 0x22};
- private ExtractorOutput extractorOutput;
- private TrackOutput trackOutput;
+ private final Id3Peeker id3Peeker;
+ private final boolean isId3MetadataDisabled;
private FlacDecoderJni decoderJni;
- private boolean metadataParsed;
+ private ExtractorOutput extractorOutput;
+ private TrackOutput trackOutput;
private ParsableByteArray outputBuffer;
private ByteBuffer outputByteBuffer;
+ private Metadata id3Metadata;
+
+ private boolean metadataParsed;
+
+ /** Constructs an instance with flags = 0. */
+ public FlacExtractor() {
+ this(0);
+ }
+
+ /**
+ * Constructs an instance.
+ *
+ * @param flags Flags that control the extractor's behavior.
+ */
+ public FlacExtractor(int flags) {
+ id3Peeker = new Id3Peeker();
+ isId3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0;
+ }
+
@Override
public void init(ExtractorOutput output) {
extractorOutput = output;
@@ -81,14 +122,19 @@ public final class FlacExtractor implements Extractor {
@Override
public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
- byte[] header = new byte[FLAC_SIGNATURE.length];
- input.peekFully(header, 0, FLAC_SIGNATURE.length);
- return Arrays.equals(header, FLAC_SIGNATURE);
+ if (input.getPosition() == 0) {
+ id3Metadata = peekId3Data(input);
+ }
+ return peekFlacSignature(input);
}
@Override
public int read(final ExtractorInput input, PositionHolder seekPosition)
throws IOException, InterruptedException {
+ if (input.getPosition() == 0 && !isId3MetadataDisabled && id3Metadata == null) {
+ id3Metadata = peekId3Data(input);
+ }
+
decoderJni.setData(input);
if (!metadataParsed) {
@@ -112,18 +158,21 @@ public final class FlacExtractor implements Extractor {
: new SeekMap.Unseekable(streamInfo.durationUs(), 0));
Format mediaFormat =
Format.createAudioSampleFormat(
- null,
+ /* id= */ null,
MimeTypes.AUDIO_RAW,
- null,
+ /* codecs= */ null,
streamInfo.bitRate(),
streamInfo.maxDecodedFrameSize(),
streamInfo.channels,
streamInfo.sampleRate,
getPcmEncoding(streamInfo.bitsPerSample),
- null,
- null,
- 0,
- null);
+ /* encoderDelay= */ 0,
+ /* encoderPadding= */ 0,
+ /* initializationData= */ null,
+ /* drmInitData= */ null,
+ /* selectionFlags= */ 0,
+ /* language= */ null,
+ isId3MetadataDisabled ? null : id3Metadata);
trackOutput.format(mediaFormat);
outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize());
@@ -170,6 +219,31 @@ public final class FlacExtractor implements Extractor {
}
}
+ /**
+ * Peeks ID3 tag data (if present) at the beginning of the input.
+ *
+ * @return The first ID3 tag decoded into a {@link Metadata} object. May be null if ID3 tag is not
+ * present in the input.
+ */
+ @Nullable
+ private Metadata peekId3Data(ExtractorInput input) throws IOException, InterruptedException {
+ input.resetPeekPosition();
+ Id3Decoder.FramePredicate id3FramePredicate =
+ isId3MetadataDisabled ? Id3Decoder.NO_FRAMES_PREDICATE : null;
+ return id3Peeker.peekId3Data(input, id3FramePredicate);
+ }
+
+ /**
+ * Peeks from the beginning of the input to see if {@link #FLAC_SIGNATURE} is present.
+ *
+ * @return Whether the input begins with {@link #FLAC_SIGNATURE}.
+ */
+ private boolean peekFlacSignature(ExtractorInput input) throws IOException, InterruptedException {
+ byte[] header = new byte[FLAC_SIGNATURE.length];
+ input.peekFully(header, 0, FLAC_SIGNATURE.length);
+ return Arrays.equals(header, FLAC_SIGNATURE);
+ }
+
private static final class FlacSeekMap implements SeekMap {
private final long durationUs;
diff --git a/extensions/flac/src/main/jni/flac_parser.cc b/extensions/flac/src/main/jni/flac_parser.cc
index b9918e7871..83d3367415 100644
--- a/extensions/flac/src/main/jni/flac_parser.cc
+++ b/extensions/flac/src/main/jni/flac_parser.cc
@@ -319,6 +319,8 @@ bool FLACParser::decodeMetadata() {
case 48000:
case 88200:
case 96000:
+ case 176400:
+ case 192000:
break;
default:
ALOGE("unsupported sample rate %u", getSampleRate());
diff --git a/extensions/gvr/build.gradle b/extensions/gvr/build.gradle
index f146ba4df6..87e72939c5 100644
--- a/extensions/gvr/build.gradle
+++ b/extensions/gvr/build.gradle
@@ -26,6 +26,7 @@ android {
dependencies {
implementation project(modulePrefix + 'library-core')
+ implementation 'com.android.support:support-annotations:' + supportLibraryVersion
implementation 'com.google.vr:sdk-audio:1.80.0'
}
diff --git a/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java b/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java
index 8d71f551cd..1b595d6886 100644
--- a/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java
+++ b/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.ext.gvr;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.Format;
@@ -39,7 +40,7 @@ public final class GvrAudioProcessor implements AudioProcessor {
private int sampleRateHz;
private int channelCount;
- private GvrAudioSurround gvrAudioSurround;
+ @Nullable private GvrAudioSurround gvrAudioSurround;
private ByteBuffer buffer;
private boolean inputEnded;
@@ -48,14 +49,13 @@ public final class GvrAudioProcessor implements AudioProcessor {
private float y;
private float z;
- /**
- * Creates a new GVR audio processor.
- */
+ /** Creates a new GVR audio processor. */
public GvrAudioProcessor() {
// Use the identity for the initial orientation.
w = 1f;
sampleRateHz = Format.NO_VALUE;
channelCount = Format.NO_VALUE;
+ buffer = EMPTY_BUFFER;
}
/**
@@ -77,9 +77,11 @@ public final class GvrAudioProcessor implements AudioProcessor {
}
}
+ @SuppressWarnings("ReferenceEquality")
@Override
- public synchronized boolean configure(int sampleRateHz, int channelCount,
- @C.Encoding int encoding) throws UnhandledFormatException {
+ public synchronized boolean configure(
+ int sampleRateHz, int channelCount, @C.Encoding int encoding)
+ throws UnhandledFormatException {
if (encoding != C.ENCODING_PCM_16BIT) {
maybeReleaseGvrAudioSurround();
throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);
@@ -116,7 +118,7 @@ public final class GvrAudioProcessor implements AudioProcessor {
gvrAudioSurround = new GvrAudioSurround(surroundFormat, sampleRateHz, channelCount,
FRAMES_PER_OUTPUT_BUFFER);
gvrAudioSurround.updateNativeOrientation(w, x, y, z);
- if (buffer == null) {
+ if (buffer == EMPTY_BUFFER) {
buffer = ByteBuffer.allocateDirect(FRAMES_PER_OUTPUT_BUFFER * OUTPUT_FRAME_SIZE)
.order(ByteOrder.nativeOrder());
}
@@ -179,10 +181,11 @@ public final class GvrAudioProcessor implements AudioProcessor {
@Override
public synchronized void reset() {
maybeReleaseGvrAudioSurround();
+ updateOrientation(/* w= */ 1f, /* x= */ 0f, /* y= */ 0f, /* z= */ 0f);
inputEnded = false;
- buffer = null;
sampleRateHz = Format.NO_VALUE;
channelCount = Format.NO_VALUE;
+ buffer = EMPTY_BUFFER;
}
private void maybeReleaseGvrAudioSurround() {
diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle
index 1a35ad3450..3529e05380 100644
--- a/extensions/ima/build.gradle
+++ b/extensions/ima/build.gradle
@@ -29,12 +29,12 @@ dependencies {
// This dependency is necessary to force the supportLibraryVersion of
// com.android.support:support-v4 to be used. Else an older version (25.2.0)
// is included via:
- // com.google.android.gms:play-services-ads:11.4.2
- // |-- com.google.android.gms:play-services-ads-lite:11.4.2
- // |-- com.google.android.gms:play-services-basement:11.4.2
- // |-- com.android.support:support-v4:25.2.0
+ // com.google.android.gms:play-services-ads:12.0.0
+ // |-- com.google.android.gms:play-services-ads-lite:12.0.0
+ // |-- com.google.android.gms:play-services-basement:12.0.0
+ // |-- com.android.support:support-v4:26.1.0
api 'com.android.support:support-v4:' + supportLibraryVersion
- api 'com.google.ads.interactivemedia.v3:interactivemedia:3.7.4'
+ api 'com.google.ads.interactivemedia.v3:interactivemedia:3.8.5'
implementation project(modulePrefix + 'library-core')
implementation 'com.google.android.gms:play-services-ads:' + playServicesLibraryVersion
}
diff --git a/extensions/ima/src/main/AndroidManifest.xml b/extensions/ima/src/main/AndroidManifest.xml
index 22fb518c58..1bb79ff21d 100644
--- a/extensions/ima/src/main/AndroidManifest.xml
+++ b/extensions/ima/src/main/AndroidManifest.xml
@@ -1,3 +1,18 @@
+
+
= 0);
this.vastLoadTimeoutMs = vastLoadTimeoutMs;
return this;
}
+ /**
+ * Sets the ad media load timeout, in milliseconds.
+ *
+ * @param mediaLoadTimeoutMs The ad media load timeout, in milliseconds.
+ * @return This builder, for convenience.
+ * @see AdsRenderingSettings#setLoadVideoTimeout(int)
+ */
+ public Builder setMediaLoadTimeoutMs(int mediaLoadTimeoutMs) {
+ Assertions.checkArgument(mediaLoadTimeoutMs >= 0);
+ this.mediaLoadTimeoutMs = mediaLoadTimeoutMs;
+ return this;
+ }
+
/**
* Returns a new {@link ImaAdsLoader} for the specified ad tag.
*
@@ -128,7 +158,14 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
* @return The new {@link ImaAdsLoader}.
*/
public ImaAdsLoader buildForAdTag(Uri adTagUri) {
- return new ImaAdsLoader(context, adTagUri, imaSdkSettings, null, vastLoadTimeoutMs);
+ return new ImaAdsLoader(
+ context,
+ adTagUri,
+ imaSdkSettings,
+ null,
+ vastLoadTimeoutMs,
+ mediaLoadTimeoutMs,
+ adEventListener);
}
/**
@@ -139,7 +176,14 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
* @return The new {@link ImaAdsLoader}.
*/
public ImaAdsLoader buildForAdsResponse(String adsResponse) {
- return new ImaAdsLoader(context, null, imaSdkSettings, adsResponse, vastLoadTimeoutMs);
+ return new ImaAdsLoader(
+ context,
+ null,
+ imaSdkSettings,
+ adsResponse,
+ vastLoadTimeoutMs,
+ mediaLoadTimeoutMs,
+ adEventListener);
}
}
@@ -174,6 +218,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
private static final String FOCUS_SKIP_BUTTON_WORKAROUND_JS = "javascript:"
+ "try{ document.getElementsByClassName(\"videoAdUiSkipButton\")[0].focus(); } catch (e) {}";
+ private static final int TIMEOUT_UNSET = -1;
+
/** The state of ad playback. */
@Retention(RetentionPolicy.SOURCE)
@IntDef({IMA_AD_STATE_NONE, IMA_AD_STATE_PLAYING, IMA_AD_STATE_PAUSED})
@@ -193,7 +239,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
private final @Nullable Uri adTagUri;
private final @Nullable String adsResponse;
- private final long vastLoadTimeoutMs;
+ 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;
@@ -209,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;
@@ -282,7 +330,14 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
* more information.
*/
public ImaAdsLoader(Context context, Uri adTagUri) {
- this(context, adTagUri, null, null, C.TIME_UNSET);
+ this(
+ context,
+ adTagUri,
+ /* imaSdkSettings= */ null,
+ /* adsResponse= */ null,
+ /* vastLoadTimeoutMs= */ TIMEOUT_UNSET,
+ /* mediaLoadTimeoutMs= */ TIMEOUT_UNSET,
+ /* adEventListener= */ null);
}
/**
@@ -298,7 +353,14 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
*/
@Deprecated
public ImaAdsLoader(Context context, Uri adTagUri, ImaSdkSettings imaSdkSettings) {
- this(context, adTagUri, imaSdkSettings, null, C.TIME_UNSET);
+ this(
+ context,
+ adTagUri,
+ imaSdkSettings,
+ /* adsResponse= */ null,
+ /* vastLoadTimeoutMs= */ TIMEOUT_UNSET,
+ /* mediaLoadTimeoutMs= */ TIMEOUT_UNSET,
+ /* adEventListener= */ null);
}
private ImaAdsLoader(
@@ -306,11 +368,15 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
@Nullable Uri adTagUri,
@Nullable ImaSdkSettings imaSdkSettings,
@Nullable String adsResponse,
- long vastLoadTimeoutMs) {
+ int vastLoadTimeoutMs,
+ 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();
@@ -361,7 +427,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
} else /* adsResponse != null */ {
request.setAdsResponse(adsResponse);
}
- if (vastLoadTimeoutMs != C.TIME_UNSET) {
+ if (vastLoadTimeoutMs != TIMEOUT_UNSET) {
request.setVastLoadTimeout(vastLoadTimeoutMs);
}
request.setAdDisplayContainer(adDisplayContainer);
@@ -466,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 {
@@ -510,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();
}
@@ -796,6 +865,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
AdsRenderingSettings adsRenderingSettings = imaSdkFactory.createAdsRenderingSettings();
adsRenderingSettings.setEnablePreloading(ENABLE_PRELOADING);
adsRenderingSettings.setMimeTypes(supportedMimeTypes);
+ if (mediaLoadTimeoutMs != TIMEOUT_UNSET) {
+ adsRenderingSettings.setLoadVideoTimeout(mediaLoadTimeoutMs);
+ }
// Set up the ad playback state, skipping ads based on the start position as required.
long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints());
@@ -900,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:
@@ -974,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) {
@@ -996,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) {
@@ -1074,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();
@@ -1098,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/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java
index 1899c815da..d3e1d9725e 100644
--- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java
+++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java
@@ -20,6 +20,7 @@ import android.support.annotation.Nullable;
import android.view.ViewGroup;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.source.BaseMediaSource;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
@@ -33,10 +34,12 @@ import java.io.IOException;
* @deprecated Use com.google.android.exoplayer2.source.ads.AdsMediaSource with ImaAdsLoader.
*/
@Deprecated
-public final class ImaAdsMediaSource implements MediaSource {
+public final class ImaAdsMediaSource extends BaseMediaSource {
private final AdsMediaSource adsMediaSource;
+ private SourceInfoRefreshListener adsMediaSourceListener;
+
/**
* Constructs a new source that inserts ads linearly with the content specified by
* {@code contentMediaSource}.
@@ -74,18 +77,16 @@ public final class ImaAdsMediaSource implements MediaSource {
}
@Override
- public void prepareSource(
- final ExoPlayer player, boolean isTopLevelSource, final Listener listener) {
- adsMediaSource.prepareSource(
- player,
- isTopLevelSource,
- new Listener() {
+ public void prepareSourceInternal(final ExoPlayer player, boolean isTopLevelSource) {
+ adsMediaSourceListener =
+ new SourceInfoRefreshListener() {
@Override
public void onSourceInfoRefreshed(
MediaSource source, Timeline timeline, @Nullable Object manifest) {
- listener.onSourceInfoRefreshed(ImaAdsMediaSource.this, timeline, manifest);
+ refreshSourceInfo(timeline, manifest);
}
- });
+ };
+ adsMediaSource.prepareSource(player, isTopLevelSource, adsMediaSourceListener);
}
@Override
@@ -104,7 +105,7 @@ public final class ImaAdsMediaSource implements MediaSource {
}
@Override
- public void releaseSource() {
- adsMediaSource.releaseSource();
+ public void releaseSourceInternal() {
+ adsMediaSource.releaseSource(adsMediaSourceListener);
}
}
diff --git a/extensions/jobdispatcher/README.md b/extensions/jobdispatcher/README.md
new file mode 100644
index 0000000000..f70125ba38
--- /dev/null
+++ b/extensions/jobdispatcher/README.md
@@ -0,0 +1,23 @@
+# ExoPlayer Firebase JobDispatcher extension #
+
+This extension provides a Scheduler implementation which uses [Firebase JobDispatcher][].
+
+[Firebase JobDispatcher]: https://github.com/firebase/firebase-jobdispatcher-android
+
+## Getting the extension ##
+
+The easiest way to use the extension is to add it as a gradle dependency:
+
+```gradle
+implementation 'com.google.android.exoplayer:extension-jobdispatcher:2.X.X'
+```
+
+where `2.X.X` is the version, which must match the version of the ExoPlayer
+library being used.
+
+Alternatively, you can clone the ExoPlayer repository and depend on the module
+locally. Instructions for doing this can be found in ExoPlayer's
+[top level README][].
+
+[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
+
diff --git a/extensions/jobdispatcher/build.gradle b/extensions/jobdispatcher/build.gradle
new file mode 100644
index 0000000000..f4a8751c67
--- /dev/null
+++ b/extensions/jobdispatcher/build.gradle
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ */
+apply from: '../../constants.gradle'
+apply plugin: 'com.android.library'
+
+android {
+ compileSdkVersion project.ext.compileSdkVersion
+ buildToolsVersion project.ext.buildToolsVersion
+
+ defaultConfig {
+ minSdkVersion project.ext.minSdkVersion
+ targetSdkVersion project.ext.targetSdkVersion
+ }
+}
+
+dependencies {
+ implementation project(modulePrefix + 'library-core')
+ implementation 'com.firebase:firebase-jobdispatcher:0.8.5'
+}
+
+ext {
+ javadocTitle = 'Firebase JobDispatcher extension'
+}
+apply from: '../../javadoc_library.gradle'
+
+ext {
+ releaseArtifact = 'extension-jobdispatcher'
+ releaseDescription = 'Firebase JobDispatcher extension for ExoPlayer.'
+}
+apply from: '../../publish.gradle'
diff --git a/extensions/jobdispatcher/src/main/AndroidManifest.xml b/extensions/jobdispatcher/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..306a087e6c
--- /dev/null
+++ b/extensions/jobdispatcher/src/main/AndroidManifest.xml
@@ -0,0 +1,18 @@
+
+
+
+
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
new file mode 100644
index 0000000000..c6701da964
--- /dev/null
+++ b/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java
@@ -0,0 +1,169 @@
+/*
+ * 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.ext.jobdispatcher;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import com.firebase.jobdispatcher.Constraint;
+import com.firebase.jobdispatcher.FirebaseJobDispatcher;
+import com.firebase.jobdispatcher.GooglePlayDriver;
+import com.firebase.jobdispatcher.Job;
+import com.firebase.jobdispatcher.Job.Builder;
+import com.firebase.jobdispatcher.JobParameters;
+import com.firebase.jobdispatcher.JobService;
+import com.firebase.jobdispatcher.Lifetime;
+import com.google.android.exoplayer2.scheduler.Requirements;
+import com.google.android.exoplayer2.scheduler.Scheduler;
+import com.google.android.exoplayer2.util.Util;
+
+/**
+ * A {@link Scheduler} that uses {@link FirebaseJobDispatcher}. To use this scheduler, you must add
+ * {@link JobDispatcherSchedulerService} to your manifest:
+ *
+ * {@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)}
+ *
+ * @see GoogleApiAvailability
+ */
+public final class JobDispatcherScheduler implements Scheduler {
+
+ private static final String TAG = "JobDispatcherScheduler";
+ 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 FirebaseJobDispatcher jobDispatcher;
+
+ /**
+ * @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, String jobTag) {
+ this.jobDispatcher =
+ new FirebaseJobDispatcher(new GooglePlayDriver(context.getApplicationContext()));
+ this.jobTag = jobTag;
+ }
+
+ @Override
+ public boolean schedule(Requirements requirements, String serviceAction, String servicePackage) {
+ Job job = buildJob(jobDispatcher, requirements, jobTag, serviceAction, servicePackage);
+ int result = jobDispatcher.schedule(job);
+ logd("Scheduling job: " + jobTag + " result: " + result);
+ return result == FirebaseJobDispatcher.SCHEDULE_RESULT_SUCCESS;
+ }
+
+ @Override
+ public boolean cancel() {
+ int result = jobDispatcher.cancel(jobTag);
+ logd("Canceling job: " + jobTag + " result: " + result);
+ return result == FirebaseJobDispatcher.CANCEL_RESULT_SUCCESS;
+ }
+
+ private static Job buildJob(
+ FirebaseJobDispatcher dispatcher,
+ Requirements requirements,
+ String tag,
+ String serviceAction,
+ String servicePackage) {
+ Builder builder =
+ dispatcher
+ .newJobBuilder()
+ .setService(JobDispatcherSchedulerService.class) // the JobService that will be called
+ .setTag(tag);
+
+ switch (requirements.getRequiredNetworkType()) {
+ case Requirements.NETWORK_TYPE_NONE:
+ // do nothing.
+ break;
+ case Requirements.NETWORK_TYPE_ANY:
+ builder.addConstraint(Constraint.ON_ANY_NETWORK);
+ break;
+ case Requirements.NETWORK_TYPE_UNMETERED:
+ builder.addConstraint(Constraint.ON_UNMETERED_NETWORK);
+ break;
+ default:
+ throw new UnsupportedOperationException();
+ }
+
+ if (requirements.isIdleRequired()) {
+ builder.addConstraint(Constraint.DEVICE_IDLE);
+ }
+ if (requirements.isChargingRequired()) {
+ builder.addConstraint(Constraint.DEVICE_CHARGING);
+ }
+ builder.setLifetime(Lifetime.FOREVER).setReplaceCurrent(true);
+
+ Bundle extras = new Bundle();
+ extras.putString(KEY_SERVICE_ACTION, serviceAction);
+ extras.putString(KEY_SERVICE_PACKAGE, servicePackage);
+ extras.putInt(KEY_REQUIREMENTS, requirements.getRequirementsData());
+ builder.setExtras(extras);
+
+ return builder.build();
+ }
+
+ private static void logd(String message) {
+ if (DEBUG) {
+ Log.d(TAG, message);
+ }
+ }
+
+ /** 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(KEY_REQUIREMENTS));
+ if (requirements.checkRequirements(this)) {
+ 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);
+ Util.startForegroundService(this, intent);
+ } else {
+ logd("Requirements are not met");
+ jobFinished(params, /* needsReschedule */ true);
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onStopJob(JobParameters params) {
+ 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 544644d03b..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;
@@ -674,12 +684,8 @@ public final class MediaSessionConnector {
// active queue item and queue navigation actions may need to be updated
updateMediaSessionPlaybackState();
}
- if (currentWindowCount != windowCount) {
- // active queue item and queue navigation actions may need to be updated
- updateMediaSessionPlaybackState();
- }
currentWindowCount = windowCount;
- currentWindowIndex = player.getCurrentWindowIndex();
+ currentWindowIndex = windowIndex;
updateMediaSessionMetadata();
}
@@ -703,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/TimelineQueueEditor.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java
index 402abf7c70..853750077d 100644
--- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java
+++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java
@@ -24,21 +24,21 @@ import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Player;
-import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource;
+import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.util.Util;
import java.util.List;
/**
- * A {@link MediaSessionConnector.QueueEditor} implementation based on the
- * {@link DynamicConcatenatingMediaSource}.
- *
- * This class implements the {@link MediaSessionConnector.CommandReceiver} interface and handles
+ * A {@link MediaSessionConnector.QueueEditor} implementation based on the {@link
+ * ConcatenatingMediaSource}.
+ *
+ *
This class implements the {@link MediaSessionConnector.CommandReceiver} interface and handles
* the {@link #COMMAND_MOVE_QUEUE_ITEM} to move a queue item instead of removing and inserting it.
* This allows to move the currently playing window without interrupting playback.
*/
-public final class TimelineQueueEditor implements MediaSessionConnector.QueueEditor,
- MediaSessionConnector.CommandReceiver {
+public final class TimelineQueueEditor
+ implements MediaSessionConnector.QueueEditor, MediaSessionConnector.CommandReceiver {
public static final String COMMAND_MOVE_QUEUE_ITEM = "exo_move_window";
public static final String EXTRA_FROM_INDEX = "from_index";
@@ -124,20 +124,21 @@ public final class TimelineQueueEditor implements MediaSessionConnector.QueueEdi
private final QueueDataAdapter queueDataAdapter;
private final MediaSourceFactory sourceFactory;
private final MediaDescriptionEqualityChecker equalityChecker;
- private final DynamicConcatenatingMediaSource queueMediaSource;
+ private final ConcatenatingMediaSource queueMediaSource;
/**
* Creates a new {@link TimelineQueueEditor} with a given mediaSourceFactory.
*
* @param mediaController A {@link MediaControllerCompat} to read the current queue.
- * @param queueMediaSource The {@link DynamicConcatenatingMediaSource} to
- * manipulate.
+ * @param queueMediaSource The {@link ConcatenatingMediaSource} to manipulate.
* @param queueDataAdapter A {@link QueueDataAdapter} to change the backing data.
* @param sourceFactory The {@link MediaSourceFactory} to build media sources.
*/
- public TimelineQueueEditor(@NonNull MediaControllerCompat mediaController,
- @NonNull DynamicConcatenatingMediaSource queueMediaSource,
- @NonNull QueueDataAdapter queueDataAdapter, @NonNull MediaSourceFactory sourceFactory) {
+ public TimelineQueueEditor(
+ @NonNull MediaControllerCompat mediaController,
+ @NonNull ConcatenatingMediaSource queueMediaSource,
+ @NonNull QueueDataAdapter queueDataAdapter,
+ @NonNull MediaSourceFactory sourceFactory) {
this(mediaController, queueMediaSource, queueDataAdapter, sourceFactory,
new MediaIdEqualityChecker());
}
@@ -146,15 +147,16 @@ public final class TimelineQueueEditor implements MediaSessionConnector.QueueEdi
* Creates a new {@link TimelineQueueEditor} with a given mediaSourceFactory.
*
* @param mediaController A {@link MediaControllerCompat} to read the current queue.
- * @param queueMediaSource The {@link DynamicConcatenatingMediaSource} to
- * manipulate.
+ * @param queueMediaSource The {@link ConcatenatingMediaSource} to manipulate.
* @param queueDataAdapter A {@link QueueDataAdapter} to change the backing data.
* @param sourceFactory The {@link MediaSourceFactory} to build media sources.
* @param equalityChecker The {@link MediaDescriptionEqualityChecker} to match queue items.
*/
- public TimelineQueueEditor(@NonNull MediaControllerCompat mediaController,
- @NonNull DynamicConcatenatingMediaSource queueMediaSource,
- @NonNull QueueDataAdapter queueDataAdapter, @NonNull MediaSourceFactory sourceFactory,
+ public TimelineQueueEditor(
+ @NonNull MediaControllerCompat mediaController,
+ @NonNull ConcatenatingMediaSource queueMediaSource,
+ @NonNull QueueDataAdapter queueDataAdapter,
+ @NonNull MediaSourceFactory sourceFactory,
@NonNull MediaDescriptionEqualityChecker equalityChecker) {
this.mediaController = mediaController;
this.queueMediaSource = queueMediaSource;
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 65bc1e89d8..92d171cfdc 100644
--- a/extensions/mediasession/src/main/res/values-af/strings.xml
+++ b/extensions/mediasession/src/main/res/values-af/strings.xml
@@ -1,22 +1,6 @@
-
-
-
-
- "Herhaal niks"
- "Herhaal een"
- "Herhaal alles"
+
+
+ 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 0dc20aaa04..54509a65ab 100644
--- a/extensions/mediasession/src/main/res/values-am/strings.xml
+++ b/extensions/mediasession/src/main/res/values-am/strings.xml
@@ -1,22 +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 2776e28356..707ad41a16 100644
--- a/extensions/mediasession/src/main/res/values-ar/strings.xml
+++ b/extensions/mediasession/src/main/res/values-ar/strings.xml
@@ -1,22 +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 d20b16531a..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,22 +1,6 @@
-
-
-
-
- "Ne ponavljaj nijednu"
- "Ponovi jednu"
- "Ponovi sve"
+
+
+ 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 087eaee8c2..8a639c6cff 100644
--- a/extensions/mediasession/src/main/res/values-bg/strings.xml
+++ b/extensions/mediasession/src/main/res/values-bg/strings.xml
@@ -1,22 +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 4a4d8646a2..cdb41b2b0a 100644
--- a/extensions/mediasession/src/main/res/values-ca/strings.xml
+++ b/extensions/mediasession/src/main/res/values-ca/strings.xml
@@ -1,22 +1,6 @@
-
-
-
-
- "No en repeteixis cap"
- "Repeteix una"
- "Repeteix tot"
+
+
+ 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 c59dcfc874..4d25b3a3ba 100644
--- a/extensions/mediasession/src/main/res/values-cs/strings.xml
+++ b/extensions/mediasession/src/main/res/values-cs/strings.xml
@@ -1,22 +1,6 @@
-
-
-
-
- "Neopakovat"
- "Opakovat jednu"
- "Opakovat vše"
+
+
+ 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 0d31261f3d..f74409a50b 100644
--- a/extensions/mediasession/src/main/res/values-da/strings.xml
+++ b/extensions/mediasession/src/main/res/values-da/strings.xml
@@ -1,22 +1,6 @@
-
-
-
-
- "Gentag ingen"
- "Gentag én"
- "Gentag alle"
+
+
+ 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 dfa86a54d4..af3564cb41 100644
--- a/extensions/mediasession/src/main/res/values-de/strings.xml
+++ b/extensions/mediasession/src/main/res/values-de/strings.xml
@@ -1,22 +1,6 @@
-
-
-
-
- "Keinen wiederholen"
- "Einen wiederholen"
- "Alle 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 e73b24592e..e4f6666622 100644
--- a/extensions/mediasession/src/main/res/values-el/strings.xml
+++ b/extensions/mediasession/src/main/res/values-el/strings.xml
@@ -1,22 +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 197222473d..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,22 +1,6 @@
-
-
-
-
- "Repeat none"
- "Repeat one"
- "Repeat all"
+
+
+ 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 197222473d..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,22 +1,6 @@
-
-
-
-
- "Repeat none"
- "Repeat one"
- "Repeat all"
+
+
+ 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 197222473d..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,22 +1,6 @@
-
-
-
-
- "Repeat none"
- "Repeat one"
- "Repeat all"
+
+
+ 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 192ad2f2ef..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,22 +1,6 @@
-
-
-
-
- "No repetir"
- "Repetir uno"
- "Repetir todo"
+
+
+ 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 192ad2f2ef..700e6de4e2 100644
--- a/extensions/mediasession/src/main/res/values-es/strings.xml
+++ b/extensions/mediasession/src/main/res/values-es/strings.xml
@@ -1,22 +1,6 @@
-
-
-
-
- "No repetir"
- "Repetir uno"
- "Repetir todo"
+
+
+ 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 42b1b14c90..96e8a1e819 100644
--- a/extensions/mediasession/src/main/res/values-fa/strings.xml
+++ b/extensions/mediasession/src/main/res/values-fa/strings.xml
@@ -1,22 +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 68f1b6c93b..db1aca3f5c 100644
--- a/extensions/mediasession/src/main/res/values-fi/strings.xml
+++ b/extensions/mediasession/src/main/res/values-fi/strings.xml
@@ -1,22 +1,6 @@
-
-
-
-
- "Ei uudelleentoistoa"
- "Toista yksi uudelleen"
- "Toista kaikki uudelleen"
+
+
+ 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 62edf759bb..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,22 +1,6 @@
-
-
-
-
- "Ne rien lire en boucle"
- "Lire une chanson en boucle"
- "Tout lire en boucle"
+
+
+ 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 2ea8653e93..9e35e35a0c 100644
--- a/extensions/mediasession/src/main/res/values-fr/strings.xml
+++ b/extensions/mediasession/src/main/res/values-fr/strings.xml
@@ -1,22 +1,6 @@
-
-
-
-
- "Ne rien lire en boucle"
- "Lire un titre en boucle"
- "Tout lire en boucle"
+
+
+ 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 79261e4e59..66415ed45d 100644
--- a/extensions/mediasession/src/main/res/values-hi/strings.xml
+++ b/extensions/mediasession/src/main/res/values-hi/strings.xml
@@ -1,22 +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 81bb428528..3b3f8170db 100644
--- a/extensions/mediasession/src/main/res/values-hr/strings.xml
+++ b/extensions/mediasession/src/main/res/values-hr/strings.xml
@@ -1,22 +1,6 @@
-
-
-
-
- "Bez ponavljanja"
- "Ponovi jedno"
- "Ponovi sve"
+
+
+ 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 8e8369a61f..392959a462 100644
--- a/extensions/mediasession/src/main/res/values-hu/strings.xml
+++ b/extensions/mediasession/src/main/res/values-hu/strings.xml
@@ -1,22 +1,6 @@
-
-
-
-
- "Nincs ismétlés"
- "Egy szám ismétlése"
- "Összes szám 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 a20a6362c8..1388877293 100644
--- a/extensions/mediasession/src/main/res/values-in/strings.xml
+++ b/extensions/mediasession/src/main/res/values-in/strings.xml
@@ -1,22 +1,6 @@
-
-
-
-
- "Jangan ulangi"
- "Ulangi 1"
- "Ulangi semua"
+
+
+ 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 3a59bb5804..8922453204 100644
--- a/extensions/mediasession/src/main/res/values-it/strings.xml
+++ b/extensions/mediasession/src/main/res/values-it/strings.xml
@@ -1,22 +1,6 @@
-
-
-
-
- "Non ripetere nulla"
- "Ripeti uno"
- "Ripeti tutto"
+
+
+ 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 f9eac73e59..193a3ac606 100644
--- a/extensions/mediasession/src/main/res/values-iw/strings.xml
+++ b/extensions/mediasession/src/main/res/values-iw/strings.xml
@@ -1,22 +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 bcfb6eb7c2..d1cd378d53 100644
--- a/extensions/mediasession/src/main/res/values-ja/strings.xml
+++ b/extensions/mediasession/src/main/res/values-ja/strings.xml
@@ -1,22 +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 7be13b133a..b561abc1d7 100644
--- a/extensions/mediasession/src/main/res/values-ko/strings.xml
+++ b/extensions/mediasession/src/main/res/values-ko/strings.xml
@@ -1,22 +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 78d1753ed0..20eb0e9b1f 100644
--- a/extensions/mediasession/src/main/res/values-lt/strings.xml
+++ b/extensions/mediasession/src/main/res/values-lt/strings.xml
@@ -1,22 +1,6 @@
-
-
-
-
- "Nekartoti nieko"
- "Kartoti vieną"
- "Kartoti viską"
+
+
+ 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 085723a271..44cddec124 100644
--- a/extensions/mediasession/src/main/res/values-lv/strings.xml
+++ b/extensions/mediasession/src/main/res/values-lv/strings.xml
@@ -1,22 +1,6 @@
-
-
-
-
- "Neatkārtot nevienu"
- "Atkārtot vienu"
- "Atkārtot visu"
+
+
+ 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 2e986733fc..eab972792f 100644
--- a/extensions/mediasession/src/main/res/values-nb/strings.xml
+++ b/extensions/mediasession/src/main/res/values-nb/strings.xml
@@ -1,22 +1,6 @@
-
-
-
-
- "Ikke gjenta noen"
- "Gjenta én"
- "Gjenta alle"
+
+
+ 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 4dfc31bb98..b1309f40d6 100644
--- a/extensions/mediasession/src/main/res/values-nl/strings.xml
+++ b/extensions/mediasession/src/main/res/values-nl/strings.xml
@@ -1,22 +1,6 @@
-
-
-
-
- "Niets herhalen"
- "Eén herhalen"
- "Alles 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 37af4c1616..5654c0f095 100644
--- a/extensions/mediasession/src/main/res/values-pl/strings.xml
+++ b/extensions/mediasession/src/main/res/values-pl/strings.xml
@@ -1,22 +1,6 @@
-
-
-
-
- "Nie powtarzaj"
- "Powtórz jeden"
- "Powtórz wszystkie"
+
+
+ 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 43a4cd9e6a..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,22 +1,6 @@
-
-
-
-
- "Não repetir nenhum"
- "Repetir um"
- "Repetir tudo"
+
+
+ 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 4e7ce248cc..a858ea4fc6 100644
--- a/extensions/mediasession/src/main/res/values-pt/strings.xml
+++ b/extensions/mediasession/src/main/res/values-pt/strings.xml
@@ -1,22 +1,6 @@
-
-
-
-
- "Não repetir"
- "Repetir uma"
- "Repetir tudo"
+
+
+ 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 9345a5df35..a88088fb0c 100644
--- a/extensions/mediasession/src/main/res/values-ro/strings.xml
+++ b/extensions/mediasession/src/main/res/values-ro/strings.xml
@@ -1,22 +1,6 @@
-
-
-
-
- "Nu repetați niciunul"
- "Repetați unul"
- "Repetați-le pe toate"
+
+
+ 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 8c52ea8395..f350724813 100644
--- a/extensions/mediasession/src/main/res/values-ru/strings.xml
+++ b/extensions/mediasession/src/main/res/values-ru/strings.xml
@@ -1,22 +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 9a7cccd096..9c0235daec 100644
--- a/extensions/mediasession/src/main/res/values-sk/strings.xml
+++ b/extensions/mediasession/src/main/res/values-sk/strings.xml
@@ -1,22 +1,6 @@
-
-
-
-
- "Neopakovať"
- "Opakovať jednu"
- "Opakovať všetko"
+
+
+ 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 7bf20baa19..9ee3add8bc 100644
--- a/extensions/mediasession/src/main/res/values-sl/strings.xml
+++ b/extensions/mediasession/src/main/res/values-sl/strings.xml
@@ -1,22 +1,6 @@
-
-
-
-
- "Brez ponavljanja"
- "Ponavljanje ene"
- "Ponavljanje vseh"
+
+
+ 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 b82940da2e..71edd5c341 100644
--- a/extensions/mediasession/src/main/res/values-sr/strings.xml
+++ b/extensions/mediasession/src/main/res/values-sr/strings.xml
@@ -1,22 +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 13edc46d1f..0956ac9fc7 100644
--- a/extensions/mediasession/src/main/res/values-sv/strings.xml
+++ b/extensions/mediasession/src/main/res/values-sv/strings.xml
@@ -1,22 +1,6 @@
-
-
-
-
- "Upprepa inga"
- "Upprepa en"
- "Upprepa alla"
+
+
+ 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 b40ce1a727..0010774a6f 100644
--- a/extensions/mediasession/src/main/res/values-sw/strings.xml
+++ b/extensions/mediasession/src/main/res/values-sw/strings.xml
@@ -1,22 +1,6 @@
-
-
-
-
- "Usirudie yoyote"
- "Rudia moja"
- "Rudia zote"
+
+
+ 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 4e40f559d0..bec0410a44 100644
--- a/extensions/mediasession/src/main/res/values-th/strings.xml
+++ b/extensions/mediasession/src/main/res/values-th/strings.xml
@@ -1,22 +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 4fff164f9f..6f8d8f4f88 100644
--- a/extensions/mediasession/src/main/res/values-tl/strings.xml
+++ b/extensions/mediasession/src/main/res/values-tl/strings.xml
@@ -1,22 +1,6 @@
-
-
-
-
- "Walang uulitin"
- "Mag-ulit ng isa"
- "Ulitin lahat"
+
+
+ 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 f93fd7fc80..20c05d9fa6 100644
--- a/extensions/mediasession/src/main/res/values-tr/strings.xml
+++ b/extensions/mediasession/src/main/res/values-tr/strings.xml
@@ -1,22 +1,6 @@
-
-
-
-
- "Hiçbirini tekrarlama"
- "Bir şarkıyı tekrarla"
- "Tümünü 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 fb9d000474..44db07ef9c 100644
--- a/extensions/mediasession/src/main/res/values-uk/strings.xml
+++ b/extensions/mediasession/src/main/res/values-uk/strings.xml
@@ -1,22 +1,6 @@
-
-
-
-
- "Не повторювати"
- "Повторити 1"
- "Повторити всі"
+
+
+ Не повторювати
+ Повторити 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 379dc36ee6..9de007cdb9 100644
--- a/extensions/mediasession/src/main/res/values-vi/strings.xml
+++ b/extensions/mediasession/src/main/res/values-vi/strings.xml
@@ -1,22 +1,6 @@
-
-
-
-
- "Không lặp lại"
- "Lặp lại một"
- "Lặp lại tất 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 6917f75bf9..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,22 +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 b63f103e2a..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,22 +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 0a460b9e08..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,22 +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 ccf8452d69..a6299ba987 100644
--- a/extensions/mediasession/src/main/res/values-zu/strings.xml
+++ b/extensions/mediasession/src/main/res/values-zu/strings.xml
@@ -1,22 +1,6 @@
-
-
-
-
- "Phinda okungekho"
- "Phinda okukodwa"
- "Phinda konke"
+
+
+ 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 0519673e50..172159b7af 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/opus/src/androidTest/AndroidManifest.xml b/extensions/opus/src/androidTest/AndroidManifest.xml
index 2d56e8d1a7..9e7f05051e 100644
--- a/extensions/opus/src/androidTest/AndroidManifest.xml
+++ b/extensions/opus/src/androidTest/AndroidManifest.xml
@@ -18,8 +18,6 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.google.android.exoplayer2.ext.opus.test">
-
-
diff --git a/extensions/vp9/src/androidTest/AndroidManifest.xml b/extensions/vp9/src/androidTest/AndroidManifest.xml
index 152ce2f533..c7ed3d7fb2 100644
--- a/extensions/vp9/src/androidTest/AndroidManifest.xml
+++ b/extensions/vp9/src/androidTest/AndroidManifest.xml
@@ -18,8 +18,6 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.google.android.exoplayer2.ext.vp9.test">
-
-
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 4d75f6076b..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
@@ -127,6 +127,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
private Bitmap bitmap;
private boolean renderedFirstFrame;
+ private long initialPositionUs;
private long joiningDeadlineMs;
private Surface surface;
private VpxOutputBufferRenderer outputBufferRenderer;
@@ -168,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);
}
/**
@@ -303,6 +311,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
inputStreamEnded = false;
outputStreamEnded = false;
clearRenderedFirstFrame();
+ initialPositionUs = C.TIME_UNSET;
consecutiveDroppedFrameCount = 0;
if (decoder != null) {
flushDecoder();
@@ -809,6 +818,10 @@ public class LibvpxVideoRenderer extends BaseRenderer {
*/
private boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs)
throws ExoPlaybackException {
+ if (initialPositionUs == C.TIME_UNSET) {
+ initialPositionUs = positionUs;
+ }
+
long earlyUs = outputBuffer.timeUs - positionUs;
if (outputMode == VpxDecoder.OUTPUT_MODE_NONE) {
// Skip frames in sync with playback, so we'll be at the right frame if the mode changes.
@@ -828,7 +841,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
return true;
}
- if (!isStarted) {
+ if (!isStarted || positionUs == initialPositionUs) {
return false;
}
diff --git a/extensions/vp9/src/main/jni/vpx_jni.cc b/extensions/vp9/src/main/jni/vpx_jni.cc
index 421b16d26d..12bc30112d 100644
--- a/extensions/vp9/src/main/jni/vpx_jni.cc
+++ b/extensions/vp9/src/main/jni/vpx_jni.cc
@@ -295,7 +295,11 @@ DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter) {
return 0;
}
if (disableLoopFilter) {
- vpx_codec_control_(context, VP9_SET_SKIP_LOOP_FILTER, true);
+ // TODO(b/71930387): Use vpx_codec_control(), not vpx_codec_control_().
+ err = vpx_codec_control_(context, VP9_SET_SKIP_LOOP_FILTER, true);
+ if (err) {
+ LOGE("ERROR: Failed to shut off libvpx loop filter, error = %d.", err);
+ }
}
// Populate JNI References.
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/androidTest/AndroidManifest.xml b/library/core/src/androidTest/AndroidManifest.xml
index 38ae6b0b2d..1aa47c10f6 100644
--- a/library/core/src/androidTest/AndroidManifest.xml
+++ b/library/core/src/androidTest/AndroidManifest.xml
@@ -18,8 +18,6 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.google.android.exoplayer2.core.test">
-
-
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java
index 9791fdb46f..58531346ab 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java
@@ -1,8 +1,24 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
package com.google.android.exoplayer2.upstream.cache;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
+import android.net.Uri;
import android.test.InstrumentationTestCase;
import android.util.SparseArray;
import com.google.android.exoplayer2.C;
@@ -14,23 +30,43 @@ import java.io.IOException;
import java.util.Collection;
import java.util.Set;
-/**
- * Tests {@link CachedContentIndex}.
- */
+/** Tests {@link CachedContentIndex}. */
public class CachedContentIndexTest extends InstrumentationTestCase {
private final byte[] testIndexV1File = {
0, 0, 0, 1, // version
0, 0, 0, 0, // flags
0, 0, 0, 2, // number_of_CachedContent
- 0, 0, 0, 5, // cache_id
- 0, 5, 65, 66, 67, 68, 69, // cache_key
+ 0, 0, 0, 5, // cache_id 5
+ 0, 5, 65, 66, 67, 68, 69, // cache_key "ABCDE"
0, 0, 0, 0, 0, 0, 0, 10, // original_content_length
- 0, 0, 0, 2, // cache_id
- 0, 5, 75, 76, 77, 78, 79, // cache_key
+ 0, 0, 0, 2, // cache_id 2
+ 0, 5, 75, 76, 77, 78, 79, // cache_key "KLMNO"
0, 0, 0, 0, 0, 0, 10, 0, // original_content_length
(byte) 0xF6, (byte) 0xFB, 0x50, 0x41 // hashcode_of_CachedContent_array
};
+
+ private final byte[] testIndexV2File = {
+ 0, 0, 0, 2, // version
+ 0, 0, 0, 0, // flags
+ 0, 0, 0, 2, // number_of_CachedContent
+ 0, 0, 0, 5, // cache_id 5
+ 0, 5, 65, 66, 67, 68, 69, // cache_key "ABCDE"
+ 0, 0, 0, 2, // metadata count
+ 0, 9, 101, 120, 111, 95, 114, 101, 100, 105, 114, // "exo_redir"
+ 0, 0, 0, 5, // value length
+ 97, 98, 99, 100, 101, // Redirected Uri "abcde"
+ 0, 7, 101, 120, 111, 95, 108, 101, 110, // "exo_len"
+ 0, 0, 0, 8, // value length
+ 0, 0, 0, 0, 0, 0, 0, 10, // original_content_length
+ 0, 0, 0, 2, // cache_id 2
+ 0, 5, 75, 76, 77, 78, 79, // cache_key "KLMNO"
+ 0, 0, 0, 1, // metadata count
+ 0, 7, 101, 120, 111, 95, 108, 101, 110, // "exo_len"
+ 0, 0, 0, 8, // value length
+ 0, 0, 0, 0, 0, 0, 10, 0, // original_content_length
+ 0x12, 0x15, 0x66, (byte) 0x8A // hashcode_of_CachedContent_array
+ };
private CachedContentIndex index;
private File cacheDir;
@@ -53,14 +89,13 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
final String key3 = "key3";
// Add two CachedContents with add methods
- CachedContent cachedContent1 = new CachedContent(5, key1, 10);
- index.addNew(cachedContent1);
+ CachedContent cachedContent1 = index.getOrAdd(key1);
CachedContent cachedContent2 = index.getOrAdd(key2);
assertThat(cachedContent1.id != cachedContent2.id).isTrue();
// add a span
- File cacheSpanFile = SimpleCacheSpanTest
- .createCacheSpanFile(cacheDir, cachedContent1.id, 10, 20, 30);
+ File cacheSpanFile =
+ SimpleCacheSpanTest.createCacheSpanFile(cacheDir, cachedContent1.id, 10, 20, 30);
SimpleCacheSpan span = SimpleCacheSpan.createCacheEntry(cacheSpanFile, index);
assertThat(span).isNotNull();
cachedContent1.addSpan(span);
@@ -90,7 +125,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
assertThat(cacheSpanFile.exists()).isTrue();
// test removeEmpty()
- index.addNew(cachedContent2);
+ index.getOrAdd(key2);
index.removeEmpty();
assertThat(index.get(key1)).isEqualTo(cachedContent1);
assertThat(index.get(key2)).isNull();
@@ -108,27 +143,32 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
index.load();
assertThat(index.getAll()).hasSize(2);
+
assertThat(index.assignIdForKey("ABCDE")).isEqualTo(5);
- assertThat(index.getContentLength("ABCDE")).isEqualTo(10);
+ ContentMetadata metadata = index.get("ABCDE").getMetadata();
+ assertThat(ContentMetadataInternal.getContentLength(metadata)).isEqualTo(10);
+
assertThat(index.assignIdForKey("KLMNO")).isEqualTo(2);
- assertThat(index.getContentLength("KLMNO")).isEqualTo(2560);
+ ContentMetadata metadata2 = index.get("KLMNO").getMetadata();
+ assertThat(ContentMetadataInternal.getContentLength(metadata2)).isEqualTo(2560);
}
- public void testStoreV1() throws Exception {
- index.addNew(new CachedContent(2, "KLMNO", 2560));
- index.addNew(new CachedContent(5, "ABCDE", 10));
-
- index.store();
-
- byte[] buffer = new byte[testIndexV1File.length];
- FileInputStream fos = new FileInputStream(new File(cacheDir, CachedContentIndex.FILE_NAME));
- assertThat(fos.read(buffer)).isEqualTo(testIndexV1File.length);
- assertThat(fos.read()).isEqualTo(-1);
+ public void testLoadV2() throws Exception {
+ FileOutputStream fos = new FileOutputStream(new File(cacheDir, CachedContentIndex.FILE_NAME));
+ fos.write(testIndexV2File);
fos.close();
- // TODO: The order of the CachedContent stored in index file isn't defined so this test may fail
- // on a different implementation of the underlying set
- assertThat(buffer).isEqualTo(testIndexV1File);
+ index.load();
+ assertThat(index.getAll()).hasSize(2);
+
+ assertThat(index.assignIdForKey("ABCDE")).isEqualTo(5);
+ ContentMetadata metadata = index.get("ABCDE").getMetadata();
+ assertThat(ContentMetadataInternal.getContentLength(metadata)).isEqualTo(10);
+ assertThat(ContentMetadataInternal.getRedirectedUri(metadata)).isEqualTo(Uri.parse("abcde"));
+
+ assertThat(index.assignIdForKey("KLMNO")).isEqualTo(2);
+ ContentMetadata metadata2 = index.get("KLMNO").getMetadata();
+ assertThat(ContentMetadataInternal.getContentLength(metadata2)).isEqualTo(2560);
}
public void testAssignIdForKeyAndGetKeyForId() throws Exception {
@@ -143,13 +183,6 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
assertThat(index.assignIdForKey(key2)).isEqualTo(id2);
}
- public void testSetGetContentLength() throws Exception {
- final String key1 = "key1";
- assertThat(index.getContentLength(key1)).isEqualTo(C.LENGTH_UNSET);
- index.setContentLength(key1, 10);
- assertThat(index.getContentLength(key1)).isEqualTo(10);
- }
-
public void testGetNewId() throws Exception {
SparseArray idToKey = new SparseArray<>();
assertThat(CachedContentIndex.getNewId(idToKey)).isEqualTo(0);
@@ -165,8 +198,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key
byte[] key2 = "Foo12345Foo12345".getBytes(C.UTF8_NAME); // 128 bit key
- assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir, key),
- new CachedContentIndex(cacheDir, key));
+ assertStoredAndLoadedEqual(
+ new CachedContentIndex(cacheDir, key), new CachedContentIndex(cacheDir, key));
// Rename the index file from the test above
File file1 = new File(cacheDir, CachedContentIndex.FILE_NAME);
@@ -174,8 +207,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
assertThat(file1.renameTo(file2)).isTrue();
// Write a new index file
- assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir, key),
- new CachedContentIndex(cacheDir, key));
+ assertStoredAndLoadedEqual(
+ new CachedContentIndex(cacheDir, key), new CachedContentIndex(cacheDir, key));
assertThat(file1.length()).isEqualTo(file2.length());
// Assert file content is different
@@ -187,8 +220,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
boolean threw = false;
try {
- assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir, key),
- new CachedContentIndex(cacheDir, key2));
+ assertStoredAndLoadedEqual(
+ new CachedContentIndex(cacheDir, key), new CachedContentIndex(cacheDir, key2));
} catch (AssertionError e) {
threw = true;
}
@@ -197,8 +230,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
.isTrue();
try {
- assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir, key),
- new CachedContentIndex(cacheDir));
+ assertStoredAndLoadedEqual(
+ new CachedContentIndex(cacheDir, key), new CachedContentIndex(cacheDir));
} catch (AssertionError e) {
threw = true;
}
@@ -207,19 +240,18 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
.isTrue();
// Non encrypted index file can be read even when encryption key provided.
- assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir),
- new CachedContentIndex(cacheDir, key));
+ assertStoredAndLoadedEqual(
+ new CachedContentIndex(cacheDir), new CachedContentIndex(cacheDir, key));
// Test multiple store() calls
CachedContentIndex index = new CachedContentIndex(cacheDir, key);
- index.addNew(new CachedContent(15, "key3", 110));
+ index.getOrAdd("key3");
index.store();
assertStoredAndLoadedEqual(index, new CachedContentIndex(cacheDir, key));
}
public void testRemoveEmptyNotLockedCachedContent() throws Exception {
- CachedContent cachedContent = new CachedContent(5, "key1", 10);
- index.addNew(cachedContent);
+ CachedContent cachedContent = index.getOrAdd("key1");
index.maybeRemove(cachedContent.key);
@@ -227,8 +259,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
}
public void testCantRemoveNotEmptyCachedContent() throws Exception {
- CachedContent cachedContent = new CachedContent(5, "key1", 10);
- index.addNew(cachedContent);
+ CachedContent cachedContent = index.getOrAdd("key1");
File cacheSpanFile =
SimpleCacheSpanTest.createCacheSpanFile(cacheDir, cachedContent.id, 10, 20, 30);
SimpleCacheSpan span = SimpleCacheSpan.createCacheEntry(cacheSpanFile, index);
@@ -240,9 +271,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
}
public void testCantRemoveLockedCachedContent() throws Exception {
- CachedContent cachedContent = new CachedContent(5, "key1", 10);
+ CachedContent cachedContent = index.getOrAdd("key1");
cachedContent.setLocked(true);
- index.addNew(cachedContent);
index.maybeRemove(cachedContent.key);
@@ -251,8 +281,13 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
private void assertStoredAndLoadedEqual(CachedContentIndex index, CachedContentIndex index2)
throws IOException {
- index.addNew(new CachedContent(5, "key1", 10));
- index.getOrAdd("key2");
+ ContentMetadataMutations mutations1 = new ContentMetadataMutations();
+ ContentMetadataInternal.setContentLength(mutations1, 2560);
+ index.getOrAdd("KLMNO").applyMetadataMutations(mutations1);
+ ContentMetadataMutations mutations2 = new ContentMetadataMutations();
+ ContentMetadataInternal.setContentLength(mutations2, 10);
+ ContentMetadataInternal.setRedirectedUri(mutations2, Uri.parse("abcde"));
+ index.getOrAdd("ABCDE").applyMetadataMutations(mutations2);
index.store();
index2.load();
@@ -260,9 +295,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
Set keys2 = index2.getKeys();
assertThat(keys2).isEqualTo(keys);
for (String key : keys) {
- assertThat(index2.getContentLength(key)).isEqualTo(index.getContentLength(key));
- assertThat(index2.get(key).getSpans()).isEqualTo(index.get(key).getSpans());
+ assertThat(index2.get(key)).isEqualTo(index.get(key));
}
}
-
}
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/BaseRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java
index 8ee9a13c55..cb917b9b79 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java
@@ -35,6 +35,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
private int index;
private int state;
private SampleStream stream;
+ private Format[] streamFormats;
private long streamOffsetUs;
private boolean readEndOfStream;
private boolean streamIsFinal;
@@ -98,6 +99,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
Assertions.checkState(!streamIsFinal);
this.stream = stream;
readEndOfStream = false;
+ streamFormats = formats;
streamOffsetUs = offsetUs;
onStreamChanged(formats, offsetUs);
}
@@ -146,6 +148,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
Assertions.checkState(state == STATE_ENABLED);
state = STATE_DISABLED;
stream = null;
+ streamFormats = null;
streamIsFinal = false;
onDisabled();
}
@@ -246,6 +249,11 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
// Methods to be called by subclasses.
+ /** Returns the formats of the currently enabled stream. */
+ protected final Format[] getStreamFormats() {
+ return streamFormats;
+ }
+
/**
* Returns the configuration set when the renderer was most recently enabled.
*/
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/ControlDispatcher.java b/library/core/src/main/java/com/google/android/exoplayer2/ControlDispatcher.java
index 21c596e6d4..f8749fc1a8 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ControlDispatcher.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ControlDispatcher.java
@@ -64,4 +64,12 @@ public interface ControlDispatcher {
*/
boolean dispatchSetShuffleModeEnabled(Player player, boolean shuffleModeEnabled);
+ /**
+ * Dispatches a {@link Player#stop()} operation.
+ *
+ * @param player The {@link Player} to which the operation should be dispatched.
+ * @param reset Whether the player should be reset.
+ * @return True if the operation was dispatched. False if suppressed.
+ */
+ boolean dispatchStop(Player player, boolean reset);
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultControlDispatcher.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultControlDispatcher.java
index 84711d752a..df3ef36b88 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultControlDispatcher.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultControlDispatcher.java
@@ -47,4 +47,9 @@ public class DefaultControlDispatcher implements ControlDispatcher {
return true;
}
+ @Override
+ public boolean dispatchStop(Player player, boolean reset) {
+ player.stop(reset);
+ return true;
+ }
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java
index e8ea2f1621..b5b364a327 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java
@@ -19,6 +19,7 @@ import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DefaultAllocator;
+import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.PriorityTaskManager;
import com.google.android.exoplayer2.util.Util;
@@ -36,7 +37,7 @@ public class DefaultLoadControl implements LoadControl {
/**
* The default maximum duration of media that the player will attempt to buffer, in milliseconds.
*/
- public static final int DEFAULT_MAX_BUFFER_MS = 30000;
+ public static final int DEFAULT_MAX_BUFFER_MS = 50000;
/**
* The default duration of media that must be buffered for playback to start or resume following a
@@ -60,6 +61,116 @@ public class DefaultLoadControl implements LoadControl {
/** The default prioritization of buffer time constraints over size constraints. */
public static final boolean DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS = true;
+ /** Builder for {@link DefaultLoadControl}. */
+ public static final class Builder {
+
+ private DefaultAllocator allocator;
+ private int minBufferMs;
+ private int maxBufferMs;
+ private int bufferForPlaybackMs;
+ private int bufferForPlaybackAfterRebufferMs;
+ private int targetBufferBytes;
+ private boolean prioritizeTimeOverSizeThresholds;
+ private PriorityTaskManager priorityTaskManager;
+
+ /** Constructs a new instance. */
+ public Builder() {
+ allocator = null;
+ minBufferMs = DEFAULT_MIN_BUFFER_MS;
+ maxBufferMs = DEFAULT_MAX_BUFFER_MS;
+ bufferForPlaybackMs = DEFAULT_BUFFER_FOR_PLAYBACK_MS;
+ bufferForPlaybackAfterRebufferMs = DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS;
+ targetBufferBytes = DEFAULT_TARGET_BUFFER_BYTES;
+ prioritizeTimeOverSizeThresholds = DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS;
+ priorityTaskManager = null;
+ }
+
+ /**
+ * Sets the {@link DefaultAllocator} used by the loader.
+ *
+ * @param allocator The {@link DefaultAllocator}.
+ * @return This builder, for convenience.
+ */
+ public Builder setAllocator(DefaultAllocator allocator) {
+ this.allocator = allocator;
+ return this;
+ }
+
+ /**
+ * Sets the buffer duration parameters.
+ *
+ * @param minBufferMs The minimum duration of media that the player will attempt to ensure is
+ * buffered at all times, in milliseconds.
+ * @param maxBufferMs The maximum duration of media that the player will attempt to buffer, in
+ * milliseconds.
+ * @param bufferForPlaybackMs The duration of media that must be buffered for playback to start
+ * or resume following a user action such as a seek, in milliseconds.
+ * @param bufferForPlaybackAfterRebufferMs The default duration of media that must be buffered
+ * for playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be
+ * caused by buffer depletion rather than a user action.
+ * @return This builder, for convenience.
+ */
+ public Builder setBufferDurationsMs(
+ int minBufferMs,
+ int maxBufferMs,
+ int bufferForPlaybackMs,
+ int bufferForPlaybackAfterRebufferMs) {
+ this.minBufferMs = minBufferMs;
+ this.maxBufferMs = maxBufferMs;
+ this.bufferForPlaybackMs = bufferForPlaybackMs;
+ this.bufferForPlaybackAfterRebufferMs = bufferForPlaybackAfterRebufferMs;
+ return this;
+ }
+
+ /**
+ * Sets the target buffer size in bytes. If set to {@link C#LENGTH_UNSET}, the target buffer
+ * size will be calculated using {@link #calculateTargetBufferSize(Renderer[],
+ * TrackSelectionArray)}.
+ *
+ * @param targetBufferBytes The target buffer size in bytes.
+ * @return This builder, for convenience.
+ */
+ public Builder setTargetBufferBytes(int targetBufferBytes) {
+ this.targetBufferBytes = targetBufferBytes;
+ return this;
+ }
+
+ /**
+ * Sets whether the load control prioritizes buffer time constraints over buffer size
+ * constraints.
+ *
+ * @param prioritizeTimeOverSizeThresholds Whether the load control prioritizes buffer time
+ * constraints over buffer size constraints.
+ * @return This builder, for convenience.
+ */
+ public Builder setPrioritizeTimeOverSizeThresholds(boolean prioritizeTimeOverSizeThresholds) {
+ this.prioritizeTimeOverSizeThresholds = prioritizeTimeOverSizeThresholds;
+ return this;
+ }
+
+ /** Sets the {@link PriorityTaskManager} to use. */
+ public Builder setPriorityTaskManager(PriorityTaskManager priorityTaskManager) {
+ this.priorityTaskManager = priorityTaskManager;
+ return this;
+ }
+
+ /** Creates a {@link DefaultLoadControl}. */
+ public DefaultLoadControl createDefaultLoadControl() {
+ if (allocator == null) {
+ allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE);
+ }
+ return new DefaultLoadControl(
+ allocator,
+ minBufferMs,
+ maxBufferMs,
+ bufferForPlaybackMs,
+ bufferForPlaybackAfterRebufferMs,
+ targetBufferBytes,
+ prioritizeTimeOverSizeThresholds,
+ priorityTaskManager);
+ }
+ }
+
private final DefaultAllocator allocator;
private final long minBufferUs;
@@ -80,11 +191,8 @@ public class DefaultLoadControl implements LoadControl {
this(new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE));
}
- /**
- * Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class.
- *
- * @param allocator The {@link DefaultAllocator} used by the loader.
- */
+ /** @deprecated Use {@link Builder} instead. */
+ @Deprecated
public DefaultLoadControl(DefaultAllocator allocator) {
this(
allocator,
@@ -96,24 +204,8 @@ public class DefaultLoadControl implements LoadControl {
DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS);
}
- /**
- * Constructs a new instance.
- *
- * @param allocator The {@link DefaultAllocator} used by the loader.
- * @param minBufferMs The minimum duration of media that the player will attempt to ensure is
- * buffered at all times, in milliseconds.
- * @param maxBufferMs The maximum duration of media that the player will attempt buffer, in
- * milliseconds.
- * @param bufferForPlaybackMs The duration of media that must be buffered for playback to start or
- * resume following a user action such as a seek, in milliseconds.
- * @param bufferForPlaybackAfterRebufferMs The default duration of media that must be buffered for
- * playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be caused by
- * buffer depletion rather than a user action.
- * @param targetBufferBytes The target buffer size in bytes. If set to {@link C#LENGTH_UNSET}, the
- * target buffer size will be calculated using {@link #calculateTargetBufferSize(Renderer[],
- * TrackSelectionArray)}.
- * @param prioritizeTimeOverSizeThresholds Whether the load control prioritizes buffer time
- */
+ /** @deprecated Use {@link Builder} instead. */
+ @Deprecated
public DefaultLoadControl(
DefaultAllocator allocator,
int minBufferMs,
@@ -133,27 +225,8 @@ public class DefaultLoadControl implements LoadControl {
null);
}
- /**
- * Constructs a new instance.
- *
- * @param allocator The {@link DefaultAllocator} used by the loader.
- * @param minBufferMs The minimum duration of media that the player will attempt to ensure is
- * buffered at all times, in milliseconds.
- * @param maxBufferMs The maximum duration of media that the player will attempt buffer, in
- * milliseconds.
- * @param bufferForPlaybackMs The duration of media that must be buffered for playback to start or
- * resume following a user action such as a seek, in milliseconds.
- * @param bufferForPlaybackAfterRebufferMs The default duration of media that must be buffered for
- * playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be caused by
- * buffer depletion rather than a user action.
- * @param targetBufferBytes The target buffer size in bytes. If set to {@link C#LENGTH_UNSET}, the
- * target buffer size will be calculated using {@link #calculateTargetBufferSize(Renderer[],
- * TrackSelectionArray)}.
- * @param prioritizeTimeOverSizeThresholds Whether the load control prioritizes buffer time
- * constraints over buffer size constraints.
- * @param priorityTaskManager If not null, registers itself as a task with priority {@link
- * C#PRIORITY_PLAYBACK} during loading periods, and unregisters itself during draining
- */
+ /** @deprecated Use {@link Builder} instead. */
+ @Deprecated
public DefaultLoadControl(
DefaultAllocator allocator,
int minBufferMs,
@@ -163,6 +236,17 @@ public class DefaultLoadControl implements LoadControl {
int targetBufferBytes,
boolean prioritizeTimeOverSizeThresholds,
PriorityTaskManager priorityTaskManager) {
+ assertGreaterOrEqual(bufferForPlaybackMs, 0, "bufferForPlaybackMs", "0");
+ assertGreaterOrEqual(
+ bufferForPlaybackAfterRebufferMs, 0, "bufferForPlaybackAfterRebufferMs", "0");
+ assertGreaterOrEqual(minBufferMs, bufferForPlaybackMs, "minBufferMs", "bufferForPlaybackMs");
+ assertGreaterOrEqual(
+ minBufferMs,
+ bufferForPlaybackAfterRebufferMs,
+ "minBufferMs",
+ "bufferForPlaybackAfterRebufferMs");
+ assertGreaterOrEqual(maxBufferMs, minBufferMs, "maxBufferMs", "minBufferMs");
+
this.allocator = allocator;
minBufferUs = minBufferMs * 1000L;
maxBufferUs = maxBufferMs * 1000L;
@@ -217,18 +301,19 @@ public class DefaultLoadControl implements LoadControl {
public boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed) {
boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize;
boolean wasBuffering = isBuffering;
- if (prioritizeTimeOverSizeThresholds) {
- isBuffering =
- bufferedDurationUs < minBufferUs // below low watermark
- || (bufferedDurationUs <= maxBufferUs // between watermarks
- && isBuffering
- && !targetBufferSizeReached);
- } else {
- isBuffering =
- !targetBufferSizeReached
- && (bufferedDurationUs < minBufferUs // below low watermark
- || (bufferedDurationUs <= maxBufferUs && isBuffering)); // between watermarks
+ long minBufferUs = this.minBufferUs;
+ if (playbackSpeed > 1) {
+ // The playback speed is faster than real time, so scale up the minimum required media
+ // duration to keep enough media buffered for a playout duration of minBufferUs.
+ long mediaDurationMinBufferUs =
+ Util.getMediaDurationForPlayoutDuration(minBufferUs, playbackSpeed);
+ minBufferUs = Math.min(mediaDurationMinBufferUs, maxBufferUs);
}
+ if (bufferedDurationUs < minBufferUs) {
+ isBuffering = prioritizeTimeOverSizeThresholds || !targetBufferSizeReached;
+ } else if (bufferedDurationUs > maxBufferUs || targetBufferSizeReached) {
+ isBuffering = false;
+ } // Else don't change the buffering state
if (priorityTaskManager != null && isBuffering != wasBuffering) {
if (isBuffering) {
priorityTaskManager.add(C.PRIORITY_PLAYBACK);
@@ -280,4 +365,7 @@ public class DefaultLoadControl implements LoadControl {
}
}
+ private static void assertGreaterOrEqual(int value1, int value2, String name1, String name2) {
+ Assertions.checkArgument(value1 >= value2, name1 + " cannot be less than " + name2);
+ }
}
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 16074108b1..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
@@ -90,28 +90,37 @@ public class DefaultRenderersFactory implements RenderersFactory {
* @param context A {@link Context}.
*/
public DefaultRenderersFactory(Context context) {
- this(context, null);
+ this(context, EXTENSION_RENDERER_MODE_OFF);
}
/**
- * @param context A {@link Context}.
- * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if DRM protected
- * playbacks are not required.
+ * @deprecated Use {@link #DefaultRenderersFactory(Context)} and pass {@link DrmSessionManager}
+ * directly to {@link SimpleExoPlayer} or {@link ExoPlayerFactory}.
*/
- public DefaultRenderersFactory(Context context,
- @Nullable DrmSessionManager drmSessionManager) {
+ @Deprecated
+ public DefaultRenderersFactory(
+ Context context, @Nullable DrmSessionManager drmSessionManager) {
this(context, drmSessionManager, EXTENSION_RENDERER_MODE_OFF);
}
/**
* @param context A {@link Context}.
- * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if DRM protected
- * playbacks are not required.
- * @param extensionRendererMode The extension renderer mode, which determines if and how
- * available extension renderers are used. Note that extensions must be included in the
- * application build for them to be considered available.
+ * @param extensionRendererMode The extension renderer mode, which determines if and how available
+ * extension renderers are used. Note that extensions must be included in the application
+ * build for them to be considered available.
*/
- public DefaultRenderersFactory(Context context,
+ public DefaultRenderersFactory(
+ Context context, @ExtensionRendererMode int extensionRendererMode) {
+ this(context, null, extensionRendererMode, DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS);
+ }
+
+ /**
+ * @deprecated Use {@link #DefaultRenderersFactory(Context, int)} and pass {@link
+ * DrmSessionManager} directly to {@link SimpleExoPlayer} or {@link ExoPlayerFactory}.
+ */
+ @Deprecated
+ public DefaultRenderersFactory(
+ Context context,
@Nullable DrmSessionManager drmSessionManager,
@ExtensionRendererMode int extensionRendererMode) {
this(context, drmSessionManager, extensionRendererMode, DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS);
@@ -119,28 +128,46 @@ public class DefaultRenderersFactory implements RenderersFactory {
/**
* @param context A {@link Context}.
- * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if DRM protected
- * playbacks are not required.
- * @param extensionRendererMode The extension renderer mode, which determines if and how
- * available extension renderers are used. Note that extensions must be included in the
- * application build for them to be considered available.
- * @param allowedVideoJoiningTimeMs The maximum duration for which video renderers can attempt
- * to seamlessly join an ongoing playback.
+ * @param extensionRendererMode The extension renderer mode, which determines if and how available
+ * extension renderers are used. Note that extensions must be included in the application
+ * build for them to be considered available.
+ * @param allowedVideoJoiningTimeMs The maximum duration for which video renderers can attempt to
+ * seamlessly join an ongoing playback.
*/
- public DefaultRenderersFactory(Context context,
+ public DefaultRenderersFactory(
+ Context context,
+ @ExtensionRendererMode int extensionRendererMode,
+ long allowedVideoJoiningTimeMs) {
+ this(context, null, extensionRendererMode, allowedVideoJoiningTimeMs);
+ }
+
+ /**
+ * @deprecated Use {@link #DefaultRenderersFactory(Context, int, long)} and pass {@link
+ * DrmSessionManager} directly to {@link SimpleExoPlayer} or {@link ExoPlayerFactory}.
+ */
+ @Deprecated
+ public DefaultRenderersFactory(
+ Context context,
@Nullable DrmSessionManager drmSessionManager,
- @ExtensionRendererMode int extensionRendererMode, long allowedVideoJoiningTimeMs) {
+ @ExtensionRendererMode int extensionRendererMode,
+ long allowedVideoJoiningTimeMs) {
this.context = context;
- this.drmSessionManager = drmSessionManager;
this.extensionRendererMode = extensionRendererMode;
this.allowedVideoJoiningTimeMs = allowedVideoJoiningTimeMs;
+ this.drmSessionManager = drmSessionManager;
}
@Override
- public Renderer[] createRenderers(Handler eventHandler,
+ public Renderer[] createRenderers(
+ Handler eventHandler,
VideoRendererEventListener videoRendererEventListener,
AudioRendererEventListener audioRendererEventListener,
- TextOutput textRendererOutput, MetadataOutput metadataRendererOutput) {
+ TextOutput textRendererOutput,
+ MetadataOutput metadataRendererOutput,
+ @Nullable DrmSessionManager drmSessionManager) {
+ if (drmSessionManager == null) {
+ drmSessionManager = this.drmSessionManager;
+ }
ArrayList renderersList = new ArrayList<>();
buildVideoRenderers(context, drmSessionManager, allowedVideoJoiningTimeMs,
eventHandler, videoRendererEventListener, extensionRendererMode, renderersList);
@@ -172,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;
@@ -232,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 c13fd6cacd..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
@@ -21,7 +21,6 @@ import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer;
import com.google.android.exoplayer2.metadata.MetadataRenderer;
import com.google.android.exoplayer2.source.ClippingMediaSource;
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
-import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.LoopingMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
@@ -54,8 +53,7 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
* implementation for loading single media samples ({@link SingleSampleMediaSource}) that's
* most often used for side-loaded subtitle files, and implementations for building more
* complex MediaSources from simpler ones ({@link MergingMediaSource}, {@link
- * ConcatenatingMediaSource}, {@link DynamicConcatenatingMediaSource}, {@link
- * LoopingMediaSource} and {@link ClippingMediaSource}).
+ * ConcatenatingMediaSource}, {@link LoopingMediaSource} and {@link ClippingMediaSource}).
* {@link Renderer}s that render individual components of the media. The library
* provides default implementations for common media types ({@link MediaCodecVideoRenderer},
* {@link MediaCodecAudioRenderer}, {@link TextRenderer} and {@link MetadataRenderer}). A
@@ -187,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/ExoPlayerFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java
index 821671e34e..8095ed9c64 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java
@@ -17,6 +17,7 @@ package com.google.android.exoplayer2;
import android.content.Context;
import android.support.annotation.Nullable;
+import com.google.android.exoplayer2.analytics.AnalyticsCollector;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
import com.google.android.exoplayer2.trackselection.TrackSelector;
@@ -58,8 +59,8 @@ public final class ExoPlayerFactory {
public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector,
LoadControl loadControl,
@Nullable DrmSessionManager drmSessionManager) {
- RenderersFactory renderersFactory = new DefaultRenderersFactory(context, drmSessionManager);
- return newSimpleInstance(renderersFactory, trackSelector, loadControl);
+ RenderersFactory renderersFactory = new DefaultRenderersFactory(context);
+ return newSimpleInstance(renderersFactory, trackSelector, loadControl, drmSessionManager);
}
/**
@@ -79,9 +80,8 @@ public final class ExoPlayerFactory {
public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector,
LoadControl loadControl, @Nullable DrmSessionManager drmSessionManager,
@DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode) {
- RenderersFactory renderersFactory = new DefaultRenderersFactory(context, drmSessionManager,
- extensionRendererMode);
- return newSimpleInstance(renderersFactory, trackSelector, loadControl);
+ RenderersFactory renderersFactory = new DefaultRenderersFactory(context, extensionRendererMode);
+ return newSimpleInstance(renderersFactory, trackSelector, loadControl, drmSessionManager);
}
/**
@@ -104,9 +104,9 @@ public final class ExoPlayerFactory {
LoadControl loadControl, @Nullable DrmSessionManager drmSessionManager,
@DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode,
long allowedVideoJoiningTimeMs) {
- RenderersFactory renderersFactory = new DefaultRenderersFactory(context, drmSessionManager,
- extensionRendererMode, allowedVideoJoiningTimeMs);
- return newSimpleInstance(renderersFactory, trackSelector, loadControl);
+ RenderersFactory renderersFactory =
+ new DefaultRenderersFactory(context, extensionRendererMode, allowedVideoJoiningTimeMs);
+ return newSimpleInstance(renderersFactory, trackSelector, loadControl, drmSessionManager);
}
/**
@@ -130,6 +130,22 @@ public final class ExoPlayerFactory {
return newSimpleInstance(renderersFactory, trackSelector, new DefaultLoadControl());
}
+ /**
+ * Creates a {@link SimpleExoPlayer} instance.
+ *
+ * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance.
+ * @param trackSelector The {@link TrackSelector} that will be used by the instance.
+ * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
+ * will not be used for DRM protected playbacks.
+ */
+ public static SimpleExoPlayer newSimpleInstance(
+ RenderersFactory renderersFactory,
+ TrackSelector trackSelector,
+ @Nullable DrmSessionManager drmSessionManager) {
+ return newSimpleInstance(
+ renderersFactory, trackSelector, new DefaultLoadControl(), drmSessionManager);
+ }
+
/**
* Creates a {@link SimpleExoPlayer} instance.
*
@@ -139,7 +155,46 @@ public final class ExoPlayerFactory {
*/
public static SimpleExoPlayer newSimpleInstance(RenderersFactory renderersFactory,
TrackSelector trackSelector, LoadControl loadControl) {
- return new SimpleExoPlayer(renderersFactory, trackSelector, loadControl);
+ return new SimpleExoPlayer(
+ renderersFactory, trackSelector, loadControl, /* drmSessionManager= */ null);
+ }
+
+ /**
+ * Creates a {@link SimpleExoPlayer} instance.
+ *
+ * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance.
+ * @param trackSelector The {@link TrackSelector} that will be used by the instance.
+ * @param loadControl The {@link LoadControl} that will be used by the instance.
+ * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
+ * will not be used for DRM protected playbacks.
+ */
+ public static SimpleExoPlayer newSimpleInstance(
+ RenderersFactory renderersFactory,
+ TrackSelector trackSelector,
+ LoadControl loadControl,
+ @Nullable DrmSessionManager drmSessionManager) {
+ return new SimpleExoPlayer(renderersFactory, trackSelector, loadControl, drmSessionManager);
+ }
+
+ /**
+ * Creates a {@link SimpleExoPlayer} instance.
+ *
+ * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance.
+ * @param trackSelector The {@link TrackSelector} that will be used by the instance.
+ * @param loadControl The {@link LoadControl} that will be used by the instance.
+ * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
+ * will not be used for DRM protected playbacks.
+ * @param analyticsCollectorFactory A factory for creating the {@link AnalyticsCollector} that
+ * will collect and forward all player events.
+ */
+ public static SimpleExoPlayer newSimpleInstance(
+ RenderersFactory renderersFactory,
+ TrackSelector trackSelector,
+ LoadControl loadControl,
+ @Nullable DrmSessionManager drmSessionManager,
+ AnalyticsCollector.Factory analyticsCollectorFactory) {
+ return new SimpleExoPlayer(
+ renderersFactory, trackSelector, loadControl, drmSessionManager, analyticsCollectorFactory);
}
/**
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 0e0a6e3c26..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;
@@ -92,11 +93,9 @@ import java.util.concurrent.CopyOnWriteArraySet;
this.listeners = new CopyOnWriteArraySet<>();
emptyTrackSelectorResult =
new TrackSelectorResult(
- TrackGroupArray.EMPTY,
- new boolean[renderers.length],
- new TrackSelectionArray(new TrackSelection[renderers.length]),
- null,
- new RendererConfiguration[renderers.length]);
+ new RendererConfiguration[renderers.length],
+ new TrackSelection[renderers.length],
+ null);
window = new Timeline.Window();
period = new Timeline.Period();
playbackParameters = PlaybackParameters.DEFAULT;
@@ -108,7 +107,11 @@ import java.util.concurrent.CopyOnWriteArraySet;
}
};
playbackInfo =
- new PlaybackInfo(Timeline.EMPTY, /* startPositionUs= */ 0, emptyTrackSelectorResult);
+ new PlaybackInfo(
+ Timeline.EMPTY,
+ /* startPositionUs= */ 0,
+ TrackGroupArray.EMPTY,
+ emptyTrackSelectorResult);
internalPlayer =
new ExoPlayerImplInternal(
renderers,
@@ -154,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);
@@ -161,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);
@@ -308,6 +317,14 @@ import java.util.concurrent.CopyOnWriteArraySet;
internalPlayer.setSeekParameters(seekParameters);
}
+ @Override
+ public @Nullable Object getCurrentTag() {
+ int windowIndex = getCurrentWindowIndex();
+ return windowIndex > playbackInfo.timeline.getWindowCount()
+ ? null
+ : playbackInfo.timeline.getWindow(windowIndex, window, /* setTag= */ true).tag;
+ }
+
@Override
public void stop() {
stop(/* reset= */ false);
@@ -315,6 +332,9 @@ import java.util.concurrent.CopyOnWriteArraySet;
@Override
public void stop(boolean reset) {
+ if (reset) {
+ playbackError = null;
+ }
PlaybackInfo playbackInfo =
getResetPlaybackInfo(
/* resetPosition= */ reset,
@@ -512,7 +532,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
@Override
public TrackGroupArray getCurrentTrackGroups() {
- return playbackInfo.trackSelectorResult.groups;
+ return playbackInfo.trackGroups;
}
@Override
@@ -550,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:
@@ -616,6 +636,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
playbackInfo.contentPositionUs,
playbackState,
/* isLoading= */ false,
+ resetState ? TrackGroupArray.EMPTY : playbackInfo.trackGroups,
resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult);
}
@@ -648,7 +669,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
trackSelector.onSelectionActivated(playbackInfo.trackSelectorResult.info);
for (Player.EventListener listener : listeners) {
listener.onTracksChanged(
- playbackInfo.trackSelectorResult.groups, playbackInfo.trackSelectorResult.selections);
+ playbackInfo.trackGroups, playbackInfo.trackSelectorResult.selections);
}
}
if (isLoadingChanged) {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java
index 24bd31c62f..ceee25af82 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java
@@ -31,6 +31,7 @@ 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.SampleStream;
+import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
@@ -48,7 +49,7 @@ import java.util.Collections;
implements Handler.Callback,
MediaPeriod.Callback,
TrackSelector.InvalidationListener,
- MediaSource.Listener,
+ MediaSource.SourceInfoRefreshListener,
PlaybackParameterListener,
PlayerMessage.Sender {
@@ -81,14 +82,6 @@ import java.util.Collections;
private static final int RENDERING_INTERVAL_MS = 10;
private static final int IDLE_INTERVAL_MS = 1000;
- /**
- * Offset added to all sample timestamps read by renderers to make them non-negative. This is
- * provided for convenience of sources that may return negative timestamps due to prerolling
- * samples from a keyframe before their first sample with timestamp zero, so it must be set to a
- * value greater than or equal to the maximum key-frame interval in seekable periods.
- */
- private static final int RENDERER_TIMESTAMP_OFFSET_US = 60000000;
-
private final Renderer[] renderers;
private final RendererCapabilities[] rendererCapabilities;
private final TrackSelector trackSelector;
@@ -154,7 +147,10 @@ import java.util.Collections;
seekParameters = SeekParameters.DEFAULT;
playbackInfo =
new PlaybackInfo(
- Timeline.EMPTY, /* startPositionUs= */ C.TIME_UNSET, emptyTrackSelectorResult);
+ Timeline.EMPTY,
+ /* startPositionUs= */ C.TIME_UNSET,
+ TrackGroupArray.EMPTY,
+ emptyTrackSelectorResult);
playbackInfoUpdate = new PlaybackInfoUpdate();
rendererCapabilities = new RendererCapabilities[renderers.length];
for (int i = 0; i < renderers.length; i++) {
@@ -244,7 +240,7 @@ import java.util.Collections;
return internalPlaybackThread.getLooper();
}
- // MediaSource.Listener implementation.
+ // MediaSource.SourceInfoRefreshListener implementation.
@Override
public void onSourceInfoRefreshed(MediaSource source, Timeline timeline, Object manifest) {
@@ -691,7 +687,7 @@ import java.util.Collections;
resetRendererPosition(periodPositionUs);
maybeContinueLoading();
} else {
- queue.clear();
+ queue.clear(/* keepFrontPeriodUid= */ true);
resetRendererPosition(periodPositionUs);
}
@@ -715,7 +711,7 @@ import java.util.Collections;
private void resetRendererPosition(long periodPositionUs) throws ExoPlaybackException {
rendererPositionUs =
!queue.hasPlayingPeriod()
- ? periodPositionUs + RENDERER_TIMESTAMP_OFFSET_US
+ ? periodPositionUs
: queue.getPlayingPeriod().toRendererTime(periodPositionUs);
mediaClock.resetPosition(rendererPositionUs);
for (Renderer renderer : enabledRenderers) {
@@ -766,7 +762,7 @@ import java.util.Collections;
handler.removeMessages(MSG_DO_SOME_WORK);
rebuffering = false;
mediaClock.stop();
- rendererPositionUs = RENDERER_TIMESTAMP_OFFSET_US;
+ rendererPositionUs = 0;
for (Renderer renderer : enabledRenderers) {
try {
disableRenderer(renderer);
@@ -776,7 +772,7 @@ import java.util.Collections;
}
}
enabledRenderers = new Renderer[0];
- queue.clear();
+ queue.clear(/* keepFrontPeriodUid= */ !resetPosition);
setIsLoading(false);
if (resetPosition) {
pendingInitialSeekPosition = null;
@@ -799,10 +795,11 @@ import java.util.Collections;
resetPosition ? C.TIME_UNSET : playbackInfo.contentPositionUs,
playbackInfo.playbackState,
/* isLoading= */ false,
+ resetState ? TrackGroupArray.EMPTY : playbackInfo.trackGroups,
resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult);
if (releaseMediaSource) {
if (mediaSource != null) {
- mediaSource.releaseSource();
+ mediaSource.releaseSource(/* listener= */ this);
mediaSource = null;
}
}
@@ -1007,7 +1004,8 @@ import java.util.Collections;
long periodPositionUs =
playingPeriodHolder.applyTrackSelection(
playbackInfo.positionUs, recreateStreams, streamResetFlags);
- updateLoadControlTrackSelection(playingPeriodHolder.trackSelectorResult);
+ updateLoadControlTrackSelection(
+ playingPeriodHolder.trackGroups, playingPeriodHolder.trackSelectorResult);
if (playbackInfo.playbackState != Player.STATE_ENDED
&& periodPositionUs != playbackInfo.positionUs) {
playbackInfo = playbackInfo.fromNewPosition(playbackInfo.periodId, periodPositionUs,
@@ -1036,7 +1034,8 @@ import java.util.Collections;
}
}
playbackInfo =
- playbackInfo.copyWithTrackSelectorResult(playingPeriodHolder.trackSelectorResult);
+ playbackInfo.copyWithTrackInfo(
+ playingPeriodHolder.trackGroups, playingPeriodHolder.trackSelectorResult);
enableRenderers(rendererWasEnabledFlags, enabledRendererCount);
} else {
// Release and re-prepare/buffer periods after the one whose selection changed.
@@ -1046,7 +1045,7 @@ import java.util.Collections;
Math.max(
periodHolder.info.startPositionUs, periodHolder.toPeriodTime(rendererPositionUs));
periodHolder.applyTrackSelection(loadingPeriodPositionUs, false);
- updateLoadControlTrackSelection(periodHolder.trackSelectorResult);
+ updateLoadControlTrackSelection(periodHolder.trackGroups, periodHolder.trackSelectorResult);
}
}
if (playbackInfo.playbackState != Player.STATE_ENDED) {
@@ -1056,9 +1055,9 @@ import java.util.Collections;
}
}
- private void updateLoadControlTrackSelection(TrackSelectorResult trackSelectorResult) {
- loadControl.onTracksSelected(
- renderers, trackSelectorResult.groups, trackSelectorResult.selections);
+ private void updateLoadControlTrackSelection(
+ TrackGroupArray trackGroups, TrackSelectorResult trackSelectorResult) {
+ loadControl.onTracksSelected(renderers, trackGroups, trackSelectorResult.selections);
}
private void updateTrackSelectionPlaybackSpeed(float playbackSpeed) {
@@ -1439,7 +1438,7 @@ import java.util.Collections;
readingPeriodHolder.mediaPeriod.readDiscontinuity() != C.TIME_UNSET;
for (int i = 0; i < renderers.length; i++) {
Renderer renderer = renderers[i];
- boolean rendererWasEnabled = oldTrackSelectorResult.renderersEnabled[i];
+ boolean rendererWasEnabled = oldTrackSelectorResult.isRendererEnabled(i);
if (!rendererWasEnabled) {
// The renderer was disabled and will be enabled when we play the next period.
} else if (initialDiscontinuity) {
@@ -1448,7 +1447,7 @@ import java.util.Collections;
renderer.setCurrentStreamFinal();
} else if (!renderer.isCurrentStreamFinal()) {
TrackSelection newSelection = newTrackSelectorResult.selections.get(i);
- boolean newRendererEnabled = newTrackSelectorResult.renderersEnabled[i];
+ boolean newRendererEnabled = newTrackSelectorResult.isRendererEnabled(i);
boolean isNoSampleRenderer = rendererCapabilities[i].getTrackType() == C.TRACK_TYPE_NONE;
RendererConfiguration oldConfig = oldTrackSelectorResult.rendererConfigurations[i];
RendererConfiguration newConfig = newTrackSelectorResult.rendererConfigurations[i];
@@ -1485,7 +1484,6 @@ import java.util.Collections;
MediaPeriod mediaPeriod =
queue.enqueueNextMediaPeriod(
rendererCapabilities,
- RENDERER_TIMESTAMP_OFFSET_US,
trackSelector,
loadControl.getAllocator(),
mediaSource,
@@ -1502,9 +1500,10 @@ import java.util.Collections;
// Stale event.
return;
}
- TrackSelectorResult trackSelectorResult =
- queue.handleLoadingPeriodPrepared(mediaClock.getPlaybackParameters().speed);
- updateLoadControlTrackSelection(trackSelectorResult);
+ MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod();
+ loadingPeriodHolder.handlePrepared(mediaClock.getPlaybackParameters().speed);
+ updateLoadControlTrackSelection(
+ loadingPeriodHolder.trackGroups, loadingPeriodHolder.trackSelectorResult);
if (!queue.hasPlayingPeriod()) {
// This is the first prepared period, so start playing it.
MediaPeriodHolder playingPeriodHolder = queue.advancePlayingPeriod();
@@ -1552,11 +1551,11 @@ import java.util.Collections;
for (int i = 0; i < renderers.length; i++) {
Renderer renderer = renderers[i];
rendererWasEnabledFlags[i] = renderer.getState() != Renderer.STATE_DISABLED;
- if (newPlayingPeriodHolder.trackSelectorResult.renderersEnabled[i]) {
+ if (newPlayingPeriodHolder.trackSelectorResult.isRendererEnabled(i)) {
enabledRendererCount++;
}
if (rendererWasEnabledFlags[i]
- && (!newPlayingPeriodHolder.trackSelectorResult.renderersEnabled[i]
+ && (!newPlayingPeriodHolder.trackSelectorResult.isRendererEnabled(i)
|| (renderer.isCurrentStreamFinal()
&& renderer.getStream() == oldPlayingPeriodHolder.sampleStreams[i]))) {
// The renderer should be disabled before playing the next period, either because it's not
@@ -1566,7 +1565,8 @@ import java.util.Collections;
}
}
playbackInfo =
- playbackInfo.copyWithTrackSelectorResult(newPlayingPeriodHolder.trackSelectorResult);
+ playbackInfo.copyWithTrackInfo(
+ newPlayingPeriodHolder.trackGroups, newPlayingPeriodHolder.trackSelectorResult);
enableRenderers(rendererWasEnabledFlags, enabledRendererCount);
}
@@ -1576,7 +1576,7 @@ import java.util.Collections;
int enabledRendererCount = 0;
MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();
for (int i = 0; i < renderers.length; i++) {
- if (playingPeriodHolder.trackSelectorResult.renderersEnabled[i]) {
+ if (playingPeriodHolder.trackSelectorResult.isRendererEnabled(i)) {
enableRenderer(i, rendererWasEnabledFlags[i], enabledRendererCount++);
}
}
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 c830a246ae..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
@@ -15,17 +15,14 @@
*/
package com.google.android.exoplayer2;
-import android.annotation.SuppressLint;
-import android.annotation.TargetApi;
-import android.media.MediaFormat;
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;
import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.ColorInfo;
-import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -47,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.
@@ -77,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.
@@ -88,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.
@@ -109,14 +96,10 @@ public final class Format implements Parcelable {
public final float frameRate;
/**
* The clockwise rotation that should be applied to the video for it to be rendered in the correct
- * orientation, or {@link #NO_VALUE} if unknown or not applicable. Only 0, 90, 180 and 270 are
- * supported.
+ * orientation, or 0 if unknown or not applicable. Only 0, 90, 180 and 270 are supported.
*/
public final int rotationDegrees;
- /**
- * The width to height ratio of pixels in the video, or {@link #NO_VALUE} if unknown or not
- * applicable.
- */
+ /** The width to height ratio of pixels in the video, or 1.0 if unknown or not applicable. */
public final float pixelWidthHeightRatio;
/**
* The stereo layout for 360/3D/VR video, or {@link #NO_VALUE} if not applicable. Valid stereo
@@ -125,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.
@@ -153,11 +132,12 @@ public final class Format implements Parcelable {
@C.PcmEncoding
public final int pcmEncoding;
/**
- * The number of samples to trim from the start of the decoded audio stream.
+ * The number of frames to trim from the start of the decoded audio stream, or 0 if not
+ * applicable.
*/
public final int encoderDelay;
/**
- * The number of samples to trim from the end of the decoded audio stream.
+ * The number of frames to trim from the end of the decoded audio stream, or 0 if not applicable.
*/
public final int encoderPadding;
@@ -178,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.
@@ -193,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,
@@ -231,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,
@@ -270,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,
@@ -324,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,
@@ -363,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;
@@ -402,16 +518,17 @@ public final class Format implements Parcelable {
this.width = width;
this.height = height;
this.frameRate = frameRate;
- this.rotationDegrees = rotationDegrees;
- this.pixelWidthHeightRatio = pixelWidthHeightRatio;
+ this.rotationDegrees = rotationDegrees == Format.NO_VALUE ? 0 : rotationDegrees;
+ this.pixelWidthHeightRatio =
+ pixelWidthHeightRatio == Format.NO_VALUE ? 1 : pixelWidthHeightRatio;
this.projectionData = projectionData;
this.stereoMode = stereoMode;
this.colorInfo = colorInfo;
this.channelCount = channelCount;
this.sampleRate = sampleRate;
this.pcmEncoding = pcmEncoding;
- this.encoderDelay = encoderDelay;
- this.encoderPadding = encoderPadding;
+ this.encoderDelay = encoderDelay == Format.NO_VALUE ? 0 : encoderDelay;
+ this.encoderPadding = encoderPadding == Format.NO_VALUE ? 0 : encoderPadding;
this.selectionFlags = selectionFlags;
this.language = language;
this.accessibilityChannel = accessibilityChannel;
@@ -435,7 +552,7 @@ public final class Format implements Parcelable {
frameRate = in.readFloat();
rotationDegrees = in.readInt();
pixelWidthHeightRatio = in.readFloat();
- boolean hasProjectionData = in.readInt() != 0;
+ boolean hasProjectionData = Util.readBoolean(in);
projectionData = hasProjectionData ? in.createByteArray() : null;
stereoMode = in.readInt();
colorInfo = in.readParcelable(ColorInfo.class.getClassLoader());
@@ -474,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,
@@ -518,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,
@@ -526,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,
@@ -550,29 +667,6 @@ public final class Format implements Parcelable {
return width == NO_VALUE || height == NO_VALUE ? NO_VALUE : (width * height);
}
- /**
- * Returns a {@link MediaFormat} representation of this format.
- */
- @SuppressLint("InlinedApi")
- @TargetApi(16)
- public final MediaFormat getFrameworkMediaFormatV16() {
- MediaFormat format = new MediaFormat();
- format.setString(MediaFormat.KEY_MIME, sampleMimeType);
- maybeSetStringV16(format, MediaFormat.KEY_LANGUAGE, language);
- maybeSetIntegerV16(format, MediaFormat.KEY_MAX_INPUT_SIZE, maxInputSize);
- maybeSetIntegerV16(format, MediaFormat.KEY_WIDTH, width);
- maybeSetIntegerV16(format, MediaFormat.KEY_HEIGHT, height);
- maybeSetFloatV16(format, MediaFormat.KEY_FRAME_RATE, frameRate);
- maybeSetIntegerV16(format, "rotation-degrees", rotationDegrees);
- maybeSetIntegerV16(format, MediaFormat.KEY_CHANNEL_COUNT, channelCount);
- maybeSetIntegerV16(format, MediaFormat.KEY_SAMPLE_RATE, sampleRate);
- for (int i = 0; i < initializationData.size(); i++) {
- format.setByteBuffer("csd-" + i, ByteBuffer.wrap(initializationData.get(i)));
- }
- maybeSetColorInfoV24(format, colorInfo);
- return format;
- }
-
@Override
public String toString() {
return "Format(" + id + ", " + containerMimeType + ", " + sampleMimeType + ", " + bitrate + ", "
@@ -603,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;
}
@@ -611,24 +705,44 @@ public final class Format implements Parcelable {
return false;
}
Format other = (Format) obj;
- if (bitrate != other.bitrate || maxInputSize != other.maxInputSize
- || width != other.width || height != other.height || frameRate != other.frameRate
- || rotationDegrees != other.rotationDegrees
- || pixelWidthHeightRatio != other.pixelWidthHeightRatio || stereoMode != other.stereoMode
- || channelCount != other.channelCount || sampleRate != other.sampleRate
- || pcmEncoding != other.pcmEncoding || encoderDelay != other.encoderDelay
- || encoderPadding != other.encoderPadding || subsampleOffsetUs != other.subsampleOffsetUs
- || selectionFlags != other.selectionFlags || !Util.areEqual(id, other.id)
- || !Util.areEqual(language, other.language)
- || accessibilityChannel != other.accessibilityChannel
- || !Util.areEqual(containerMimeType, other.containerMimeType)
- || !Util.areEqual(sampleMimeType, other.sampleMimeType)
- || !Util.areEqual(codecs, other.codecs)
- || !Util.areEqual(drmInitData, other.drmInitData)
- || !Util.areEqual(metadata, other.metadata)
- || !Util.areEqual(colorInfo, other.colorInfo)
- || !Arrays.equals(projectionData, other.projectionData)
- || initializationData.size() != other.initializationData.size()) {
+ return bitrate == other.bitrate
+ && maxInputSize == other.maxInputSize
+ && width == other.width
+ && height == other.height
+ && frameRate == other.frameRate
+ && rotationDegrees == other.rotationDegrees
+ && pixelWidthHeightRatio == other.pixelWidthHeightRatio
+ && stereoMode == other.stereoMode
+ && channelCount == other.channelCount
+ && sampleRate == other.sampleRate
+ && pcmEncoding == other.pcmEncoding
+ && encoderDelay == other.encoderDelay
+ && encoderPadding == other.encoderPadding
+ && subsampleOffsetUs == other.subsampleOffsetUs
+ && selectionFlags == other.selectionFlags
+ && Util.areEqual(id, other.id)
+ && Util.areEqual(language, other.language)
+ && accessibilityChannel == other.accessibilityChannel
+ && Util.areEqual(containerMimeType, other.containerMimeType)
+ && Util.areEqual(sampleMimeType, other.sampleMimeType)
+ && Util.areEqual(codecs, other.codecs)
+ && Util.areEqual(drmInitData, other.drmInitData)
+ && Util.areEqual(metadata, other.metadata)
+ && Util.areEqual(colorInfo, other.colorInfo)
+ && Arrays.equals(projectionData, other.projectionData)
+ && initializationDataEquals(other);
+ }
+
+ /**
+ * Returns whether the {@link #initializationData}s belonging to this format and {@code other} are
+ * equal.
+ *
+ * @param other The other format whose {@link #initializationData} is being compared.
+ * @return Whether the {@link #initializationData}s belonging to this format and {@code other} are
+ * equal.
+ */
+ public boolean initializationDataEquals(Format other) {
+ if (initializationData.size() != other.initializationData.size()) {
return false;
}
for (int i = 0; i < initializationData.size(); i++) {
@@ -639,45 +753,6 @@ public final class Format implements Parcelable {
return true;
}
- @TargetApi(24)
- private static void maybeSetColorInfoV24(MediaFormat format, ColorInfo colorInfo) {
- if (colorInfo == null) {
- return;
- }
- maybeSetIntegerV16(format, MediaFormat.KEY_COLOR_TRANSFER, colorInfo.colorTransfer);
- maybeSetIntegerV16(format, MediaFormat.KEY_COLOR_STANDARD, colorInfo.colorSpace);
- maybeSetIntegerV16(format, MediaFormat.KEY_COLOR_RANGE, colorInfo.colorRange);
- maybeSetByteBufferV16(format, MediaFormat.KEY_HDR_STATIC_INFO, colorInfo.hdrStaticInfo);
- }
-
- @TargetApi(16)
- private static void maybeSetStringV16(MediaFormat format, String key, String value) {
- if (value != null) {
- format.setString(key, value);
- }
- }
-
- @TargetApi(16)
- private static void maybeSetIntegerV16(MediaFormat format, String key, int value) {
- if (value != NO_VALUE) {
- format.setInteger(key, value);
- }
- }
-
- @TargetApi(16)
- private static void maybeSetFloatV16(MediaFormat format, String key, float value) {
- if (value != NO_VALUE) {
- format.setFloat(key, value);
- }
- }
-
- @TargetApi(16)
- private static void maybeSetByteBufferV16(MediaFormat format, String key, byte[] value) {
- if (value != null) {
- format.setByteBuffer(key, ByteBuffer.wrap(value));
- }
- }
-
// Utility methods
/**
@@ -730,7 +805,7 @@ public final class Format implements Parcelable {
dest.writeFloat(frameRate);
dest.writeInt(rotationDegrees);
dest.writeFloat(pixelWidthHeightRatio);
- dest.writeInt(projectionData != null ? 1 : 0);
+ Util.writeBoolean(dest, projectionData != null);
if (projectionData != null) {
dest.writeByteArray(projectionData);
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java
index 43036b154b..2f71d0d547 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java
@@ -21,6 +21,7 @@ import com.google.android.exoplayer2.source.EmptySampleStream;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.SampleStream;
+import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.trackselection.TrackSelector;
@@ -43,6 +44,7 @@ import com.google.android.exoplayer2.util.Assertions;
public boolean hasEnabledTracks;
public MediaPeriodInfo info;
public MediaPeriodHolder next;
+ public TrackGroupArray trackGroups;
public TrackSelectorResult trackSelectorResult;
private final RendererCapabilities[] rendererCapabilities;
@@ -81,9 +83,12 @@ import com.google.android.exoplayer2.util.Assertions;
mayRetainStreamFlags = new boolean[rendererCapabilities.length];
MediaPeriod mediaPeriod = mediaSource.createPeriod(info.id, allocator);
if (info.endPositionUs != C.TIME_END_OF_SOURCE) {
- ClippingMediaPeriod clippingMediaPeriod = new ClippingMediaPeriod(mediaPeriod, true);
- clippingMediaPeriod.setClipping(0, info.endPositionUs);
- mediaPeriod = clippingMediaPeriod;
+ mediaPeriod =
+ new ClippingMediaPeriod(
+ mediaPeriod,
+ /* enableInitialDiscontinuity= */ true,
+ /* startUs= */ 0,
+ info.endPositionUs);
}
this.mediaPeriod = mediaPeriod;
}
@@ -132,13 +137,13 @@ import com.google.android.exoplayer2.util.Assertions;
return !prepared ? 0 : mediaPeriod.getNextLoadPositionUs();
}
- public TrackSelectorResult handlePrepared(float playbackSpeed) throws ExoPlaybackException {
+ public void handlePrepared(float playbackSpeed) throws ExoPlaybackException {
prepared = true;
+ trackGroups = mediaPeriod.getTrackGroups();
selectTracks(playbackSpeed);
long newStartPositionUs = applyTrackSelection(info.startPositionUs, false);
rendererPositionOffsetUs += info.startPositionUs - newStartPositionUs;
info = info.copyWithStartPositionUs(newStartPositionUs);
- return trackSelectorResult;
}
public void reevaluateBuffer(long rendererPositionUs) {
@@ -154,7 +159,7 @@ import com.google.android.exoplayer2.util.Assertions;
public boolean selectTracks(float playbackSpeed) throws ExoPlaybackException {
TrackSelectorResult selectorResult =
- trackSelector.selectTracks(rendererCapabilities, mediaPeriod.getTrackGroups());
+ trackSelector.selectTracks(rendererCapabilities, trackGroups);
if (selectorResult.isEquivalent(periodTrackSelectorResult)) {
return false;
}
@@ -174,8 +179,7 @@ import com.google.android.exoplayer2.util.Assertions;
public long applyTrackSelection(
long positionUs, boolean forceRecreateStreams, boolean[] streamResetFlags) {
- TrackSelectionArray trackSelections = trackSelectorResult.selections;
- for (int i = 0; i < trackSelections.length; i++) {
+ for (int i = 0; i < trackSelectorResult.length; i++) {
mayRetainStreamFlags[i] =
!forceRecreateStreams && trackSelectorResult.isEquivalent(periodTrackSelectorResult, i);
}
@@ -185,6 +189,7 @@ import com.google.android.exoplayer2.util.Assertions;
disassociateNoSampleRenderersWithEmptySampleStream(sampleStreams);
updatePeriodTrackSelectorResult(trackSelectorResult);
// Disable streams on the period and get new streams for updated/newly-enabled tracks.
+ TrackSelectionArray trackSelections = trackSelectorResult.selections;
positionUs =
mediaPeriod.selectTracks(
trackSelections.getAll(),
@@ -198,7 +203,7 @@ import com.google.android.exoplayer2.util.Assertions;
hasEnabledTracks = false;
for (int i = 0; i < sampleStreams.length; i++) {
if (sampleStreams[i] != null) {
- Assertions.checkState(trackSelectorResult.renderersEnabled[i]);
+ Assertions.checkState(trackSelectorResult.isRendererEnabled(i));
// hasEnabledTracks should be true only when non-empty streams exists.
if (rendererCapabilities[i].getTrackType() != C.TRACK_TYPE_NONE) {
hasEnabledTracks = true;
@@ -235,8 +240,8 @@ import com.google.android.exoplayer2.util.Assertions;
}
private void enableTrackSelectionsInResult(TrackSelectorResult trackSelectorResult) {
- for (int i = 0; i < trackSelectorResult.renderersEnabled.length; i++) {
- boolean rendererEnabled = trackSelectorResult.renderersEnabled[i];
+ for (int i = 0; i < trackSelectorResult.length; i++) {
+ boolean rendererEnabled = trackSelectorResult.isRendererEnabled(i);
TrackSelection trackSelection = trackSelectorResult.selections.get(i);
if (rendererEnabled && trackSelection != null) {
trackSelection.enable();
@@ -245,8 +250,8 @@ import com.google.android.exoplayer2.util.Assertions;
}
private void disableTrackSelectionsInResult(TrackSelectorResult trackSelectorResult) {
- for (int i = 0; i < trackSelectorResult.renderersEnabled.length; i++) {
- boolean rendererEnabled = trackSelectorResult.renderersEnabled[i];
+ for (int i = 0; i < trackSelectorResult.length; i++) {
+ boolean rendererEnabled = trackSelectorResult.isRendererEnabled(i);
TrackSelection trackSelection = trackSelectorResult.selections.get(i);
if (rendererEnabled && trackSelection != null) {
trackSelection.disable();
@@ -273,7 +278,7 @@ import com.google.android.exoplayer2.util.Assertions;
private void associateNoSampleRenderersWithEmptySampleStream(SampleStream[] sampleStreams) {
for (int i = 0; i < rendererCapabilities.length; i++) {
if (rendererCapabilities[i].getTrackType() == C.TRACK_TYPE_NONE
- && trackSelectorResult.renderersEnabled[i]) {
+ && trackSelectorResult.isRendererEnabled(i)) {
sampleStreams[i] = new EmptySampleStream();
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java
index 3efff58f5d..717f873622 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java
@@ -22,7 +22,6 @@ 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.trackselection.TrackSelector;
-import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.util.Assertions;
@@ -52,6 +51,8 @@ import com.google.android.exoplayer2.util.Assertions;
private MediaPeriodHolder reading;
private MediaPeriodHolder loading;
private int length;
+ private Object oldFrontPeriodUid;
+ private long oldFrontPeriodWindowSequenceNumber;
/** Creates a new media period queue. */
public MediaPeriodQueue() {
@@ -130,7 +131,6 @@ import com.google.android.exoplayer2.util.Assertions;
* and returns it.
*
* @param rendererCapabilities The renderer capabilities.
- * @param rendererTimestampOffsetUs The base time offset added to for renderers.
* @param trackSelector The track selector.
* @param allocator The allocator.
* @param mediaSource The media source that produced the media period.
@@ -139,7 +139,6 @@ import com.google.android.exoplayer2.util.Assertions;
*/
public MediaPeriod enqueueNextMediaPeriod(
RendererCapabilities[] rendererCapabilities,
- long rendererTimestampOffsetUs,
TrackSelector trackSelector,
Allocator allocator,
MediaSource mediaSource,
@@ -147,7 +146,7 @@ import com.google.android.exoplayer2.util.Assertions;
MediaPeriodInfo info) {
long rendererPositionOffsetUs =
loading == null
- ? (info.startPositionUs + rendererTimestampOffsetUs)
+ ? info.startPositionUs
: (loading.getRendererOffset() + loading.info.durationUs);
MediaPeriodHolder newPeriodHolder =
new MediaPeriodHolder(
@@ -162,22 +161,12 @@ import com.google.android.exoplayer2.util.Assertions;
Assertions.checkState(hasPlayingPeriod());
loading.next = newPeriodHolder;
}
+ oldFrontPeriodUid = null;
loading = newPeriodHolder;
length++;
return newPeriodHolder.mediaPeriod;
}
- /**
- * Handles the loading media period being prepared.
- *
- * @param playbackSpeed The current playback speed.
- * @return The result of selecting tracks on the newly prepared loading media period.
- */
- public TrackSelectorResult handleLoadingPeriodPrepared(float playbackSpeed)
- throws ExoPlaybackException {
- return loading.handlePrepared(playbackSpeed);
- }
-
/**
* Returns the loading period holder which is at the end of the queue, or null if the queue is
* empty.
@@ -276,12 +265,21 @@ import com.google.android.exoplayer2.util.Assertions;
return removedReading;
}
- /** Clears the queue. */
- public void clear() {
+ /**
+ * Clears the queue.
+ *
+ * @param keepFrontPeriodUid Whether the queue should keep the id of the media period in the front
+ * of queue (typically the playing one) for later reuse.
+ */
+ public void clear(boolean keepFrontPeriodUid) {
MediaPeriodHolder front = getFrontPeriod();
if (front != null) {
+ oldFrontPeriodUid = keepFrontPeriodUid ? front.uid : null;
+ oldFrontPeriodWindowSequenceNumber = front.info.id.windowSequenceNumber;
front.release();
removeAfter(front);
+ } else if (!keepFrontPeriodUid) {
+ oldFrontPeriodUid = null;
}
playing = null;
loading = null;
@@ -408,6 +406,17 @@ import com.google.android.exoplayer2.util.Assertions;
*/
private long resolvePeriodIndexToWindowSequenceNumber(int periodIndex) {
Object periodUid = timeline.getPeriod(periodIndex, period, /* setIds= */ true).uid;
+ int windowIndex = period.windowIndex;
+ if (oldFrontPeriodUid != null) {
+ int oldFrontPeriodIndex = timeline.getIndexOfPeriod(oldFrontPeriodUid);
+ if (oldFrontPeriodIndex != C.INDEX_UNSET) {
+ int oldFrontWindowIndex = timeline.getPeriod(oldFrontPeriodIndex, period).windowIndex;
+ if (oldFrontWindowIndex == windowIndex) {
+ // Try to match old front uid after the queue has been cleared.
+ return oldFrontPeriodWindowSequenceNumber;
+ }
+ }
+ }
MediaPeriodHolder mediaPeriodHolder = getFrontPeriod();
while (mediaPeriodHolder != null) {
if (mediaPeriodHolder.uid.equals(periodUid)) {
@@ -416,7 +425,6 @@ import com.google.android.exoplayer2.util.Assertions;
}
mediaPeriodHolder = mediaPeriodHolder.next;
}
- int windowIndex = period.windowIndex;
mediaPeriodHolder = getFrontPeriod();
while (mediaPeriodHolder != null) {
int indexOfHolderInTimeline = timeline.getIndexOfPeriod(mediaPeriodHolder.uid);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java
index 3a4ee0e501..80de073e2d 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java
@@ -13,10 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
- package com.google.android.exoplayer2;
+package com.google.android.exoplayer2;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
+import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
/**
@@ -31,13 +32,17 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
public final long contentPositionUs;
public final int playbackState;
public final boolean isLoading;
+ public final TrackGroupArray trackGroups;
public final TrackSelectorResult trackSelectorResult;
public volatile long positionUs;
public volatile long bufferedPositionUs;
public PlaybackInfo(
- Timeline timeline, long startPositionUs, TrackSelectorResult trackSelectorResult) {
+ Timeline timeline,
+ long startPositionUs,
+ TrackGroupArray trackGroups,
+ TrackSelectorResult trackSelectorResult) {
this(
timeline,
/* manifest= */ null,
@@ -46,6 +51,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
/* contentPositionUs =*/ C.TIME_UNSET,
Player.STATE_IDLE,
/* isLoading= */ false,
+ trackGroups,
trackSelectorResult);
}
@@ -57,6 +63,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
long contentPositionUs,
int playbackState,
boolean isLoading,
+ TrackGroupArray trackGroups,
TrackSelectorResult trackSelectorResult) {
this.timeline = timeline;
this.manifest = manifest;
@@ -67,11 +74,12 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
this.bufferedPositionUs = startPositionUs;
this.playbackState = playbackState;
this.isLoading = isLoading;
+ this.trackGroups = trackGroups;
this.trackSelectorResult = trackSelectorResult;
}
- public PlaybackInfo fromNewPosition(MediaPeriodId periodId, long startPositionUs,
- long contentPositionUs) {
+ public PlaybackInfo fromNewPosition(
+ MediaPeriodId periodId, long startPositionUs, long contentPositionUs) {
return new PlaybackInfo(
timeline,
manifest,
@@ -80,6 +88,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
periodId.isAd() ? contentPositionUs : C.TIME_UNSET,
playbackState,
isLoading,
+ trackGroups,
trackSelectorResult);
}
@@ -93,6 +102,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
contentPositionUs,
playbackState,
isLoading,
+ trackGroups,
trackSelectorResult);
copyMutablePositions(this, playbackInfo);
return playbackInfo;
@@ -108,6 +118,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
contentPositionUs,
playbackState,
isLoading,
+ trackGroups,
trackSelectorResult);
copyMutablePositions(this, playbackInfo);
return playbackInfo;
@@ -123,6 +134,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
contentPositionUs,
playbackState,
isLoading,
+ trackGroups,
trackSelectorResult);
copyMutablePositions(this, playbackInfo);
return playbackInfo;
@@ -138,12 +150,14 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
contentPositionUs,
playbackState,
isLoading,
+ trackGroups,
trackSelectorResult);
copyMutablePositions(this, playbackInfo);
return playbackInfo;
}
- public PlaybackInfo copyWithTrackSelectorResult(TrackSelectorResult trackSelectorResult) {
+ public PlaybackInfo copyWithTrackInfo(
+ TrackGroupArray trackGroups, TrackSelectorResult trackSelectorResult) {
PlaybackInfo playbackInfo =
new PlaybackInfo(
timeline,
@@ -153,6 +167,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
contentPositionUs,
playbackState,
isLoading,
+ trackGroups,
trackSelectorResult);
copyMutablePositions(this, playbackInfo);
return playbackInfo;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java
index 47d5bc88b9..a7de96a2de 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java
@@ -23,33 +23,55 @@ import com.google.android.exoplayer2.util.Assertions;
public final class PlaybackParameters {
/**
- * The default playback parameters: real-time playback with no pitch modification.
+ * The default playback parameters: real-time playback with no pitch modification or silence
+ * skipping.
*/
- public static final PlaybackParameters DEFAULT = new PlaybackParameters(1f, 1f);
+ public static final PlaybackParameters DEFAULT = new PlaybackParameters(/* speed= */ 1f);
- /**
- * The factor by which playback will be sped up.
- */
+ /** The factor by which playback will be sped up. */
public final float speed;
- /**
- * The factor by which the audio pitch will be scaled.
- */
+ /** The factor by which the audio pitch will be scaled. */
public final float pitch;
+ /** Whether to skip silence in the input. */
+ public final boolean skipSilence;
+
private final int scaledUsPerMs;
/**
- * Creates new playback parameters.
+ * Creates new playback parameters that set the playback speed.
+ *
+ * @param speed The factor by which playback will be sped up. Must be greater than zero.
+ */
+ public PlaybackParameters(float speed) {
+ this(speed, /* pitch= */ 1f, /* skipSilence= */ false);
+ }
+
+ /**
+ * Creates new playback parameters that set the playback speed and audio pitch scaling factor.
*
* @param speed The factor by which playback will be sped up. Must be greater than zero.
* @param pitch The factor by which the audio pitch will be scaled. Must be greater than zero.
*/
public PlaybackParameters(float speed, float pitch) {
+ this(speed, pitch, /* skipSilence= */ false);
+ }
+
+ /**
+ * Creates new playback parameters that set the playback speed, audio pitch scaling factor and
+ * whether to skip silence in the audio stream.
+ *
+ * @param speed The factor by which playback will be sped up. Must be greater than zero.
+ * @param pitch The factor by which the audio pitch will be scaled. Must be greater than zero.
+ * @param skipSilence Whether to skip silences in the audio stream.
+ */
+ public PlaybackParameters(float speed, float pitch, boolean skipSilence) {
Assertions.checkArgument(speed > 0);
Assertions.checkArgument(pitch > 0);
this.speed = speed;
this.pitch = pitch;
+ this.skipSilence = skipSilence;
scaledUsPerMs = Math.round(speed * 1000f);
}
@@ -73,14 +95,17 @@ public final class PlaybackParameters {
return false;
}
PlaybackParameters other = (PlaybackParameters) obj;
- return this.speed == other.speed && this.pitch == other.pitch;
+ return this.speed == other.speed
+ && this.pitch == other.pitch
+ && this.skipSilence == other.skipSilence;
}
-
+
@Override
public int hashCode() {
int result = 17;
result = 31 * result + Float.floatToRawIntBits(speed);
result = 31 * result + Float.floatToRawIntBits(pitch);
+ result = 31 * result + (skipSilence ? 1 : 0);
return result;
}
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 443ff8a2ea..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}.
*
@@ -655,6 +666,12 @@ public interface Player {
*/
int getPreviousWindowIndex();
+ /**
+ * Returns the tag of the currently playing window in the timeline. May be null if no tag is set
+ * or the timeline is not yet available.
+ */
+ @Nullable Object getCurrentTag();
+
/**
* Returns the duration of the current window in milliseconds, or {@link C#TIME_UNSET} if the
* duration is not known.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java b/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java
index d0a07930e0..e53db4568d 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java
@@ -15,21 +15,29 @@
*/
package com.google.android.exoplayer2;
+import android.support.annotation.IntDef;
import com.google.android.exoplayer2.source.SampleStream;
import com.google.android.exoplayer2.util.MediaClock;
import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
/**
* Renders media read from a {@link SampleStream}.
*
*
Internally, a renderer's lifecycle is managed by the owning {@link ExoPlayer}. The renderer is
- * transitioned through various states as the overall playback state changes. The valid state
- * transitions are shown below, annotated with the methods that are called during each transition.
+ * transitioned through various states as the overall playback state and enabled tracks change. The
+ * valid state transitions are shown below, annotated with the methods that are called during each
+ * transition.
*
*
*/
public interface Renderer extends PlayerMessage.Target {
+ /** The renderer states. */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({STATE_DISABLED, STATE_ENABLED, STATE_STARTED})
+ @interface State {}
/**
* The renderer is disabled.
*/
@@ -80,8 +88,10 @@ public interface Renderer extends PlayerMessage.Target {
/**
* Returns the current state of the renderer.
*
- * @return The current state (one of the {@code STATE_*} constants).
+ * @return The current state. One of {@link #STATE_DISABLED}, {@link #STATE_ENABLED} and {@link
+ * #STATE_STARTED}.
*/
+ @State
int getState();
/**
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/RenderersFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/RenderersFactory.java
index 944a6a9e5e..e221898471 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/RenderersFactory.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/RenderersFactory.java
@@ -16,7 +16,10 @@
package com.google.android.exoplayer2;
import android.os.Handler;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
+import com.google.android.exoplayer2.drm.DrmSessionManager;
+import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
import com.google.android.exoplayer2.metadata.MetadataOutput;
import com.google.android.exoplayer2.text.TextOutput;
import com.google.android.exoplayer2.video.VideoRendererEventListener;
@@ -34,11 +37,14 @@ public interface RenderersFactory {
* @param audioRendererEventListener An event listener for audio renderers.
* @param textRendererOutput An output for text renderers.
* @param metadataRendererOutput An output for metadata renderers.
+ * @param drmSessionManager A drm session manager used by renderers.
* @return The {@link Renderer instances}.
*/
- Renderer[] createRenderers(Handler eventHandler,
+ Renderer[] createRenderers(
+ Handler eventHandler,
VideoRendererEventListener videoRendererEventListener,
- AudioRendererEventListener audioRendererEventListener, TextOutput textRendererOutput,
- MetadataOutput metadataRendererOutput);
-
+ AudioRendererEventListener audioRendererEventListener,
+ TextOutput textRendererOutput,
+ MetadataOutput metadataRendererOutput,
+ @Nullable DrmSessionManager drmSessionManager);
}
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 98ef35d62c..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
@@ -27,9 +27,14 @@ import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.TextureView;
+import com.google.android.exoplayer2.analytics.AnalyticsCollector;
+import com.google.android.exoplayer2.analytics.AnalyticsListener;
import com.google.android.exoplayer2.audio.AudioAttributes;
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.decoder.DecoderCounters;
+import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
+import com.google.android.exoplayer2.drm.DrmSessionManager;
+import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataOutput;
import com.google.android.exoplayer2.source.MediaSource;
@@ -42,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;
@@ -61,6 +67,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
protected final Renderer[] renderers;
private final ExoPlayer player;
+ private final Handler eventHandler;
private final ComponentListener componentListener;
private final CopyOnWriteArraySet
videoListeners;
@@ -68,6 +75,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
private final CopyOnWriteArraySet metadataOutputs;
private final CopyOnWriteArraySet videoDebugListeners;
private final CopyOnWriteArraySet audioDebugListeners;
+ private final AnalyticsCollector analyticsCollector;
private Format videoFormat;
private Format audioFormat;
@@ -83,21 +91,60 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
private int audioSessionId;
private AudioAttributes audioAttributes;
private float audioVolume;
+ private MediaSource mediaSource;
/**
* @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance.
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
* @param loadControl The {@link LoadControl} that will be used by the instance.
+ * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
+ * will not be used for DRM protected playbacks.
*/
protected SimpleExoPlayer(
- RenderersFactory renderersFactory, TrackSelector trackSelector, LoadControl loadControl) {
- this(renderersFactory, trackSelector, loadControl, Clock.DEFAULT);
+ RenderersFactory renderersFactory,
+ TrackSelector trackSelector,
+ LoadControl loadControl,
+ @Nullable DrmSessionManager drmSessionManager) {
+ this(
+ renderersFactory,
+ trackSelector,
+ loadControl,
+ drmSessionManager,
+ new AnalyticsCollector.Factory());
}
/**
* @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance.
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
* @param loadControl The {@link LoadControl} that will be used by the instance.
+ * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
+ * will not be used for DRM protected playbacks.
+ * @param analyticsCollectorFactory A factory for creating the {@link AnalyticsCollector} that
+ * will collect and forward all player events.
+ */
+ protected SimpleExoPlayer(
+ RenderersFactory renderersFactory,
+ TrackSelector trackSelector,
+ LoadControl loadControl,
+ @Nullable DrmSessionManager drmSessionManager,
+ AnalyticsCollector.Factory analyticsCollectorFactory) {
+ this(
+ renderersFactory,
+ trackSelector,
+ loadControl,
+ drmSessionManager,
+ analyticsCollectorFactory,
+ Clock.DEFAULT);
+ }
+
+ /**
+ * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance.
+ * @param trackSelector The {@link TrackSelector} that will be used by the instance.
+ * @param loadControl The {@link LoadControl} that will be used by the instance.
+ * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
+ * will not be used for DRM protected playbacks.
+ * @param analyticsCollectorFactory A factory for creating the {@link AnalyticsCollector} that
+ * will collect and forward all player events.
* @param clock The {@link Clock} that will be used by the instance. Should always be {@link
* Clock#DEFAULT}, unless the player is being used from a test.
*/
@@ -105,6 +152,8 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
RenderersFactory renderersFactory,
TrackSelector trackSelector,
LoadControl loadControl,
+ @Nullable DrmSessionManager drmSessionManager,
+ AnalyticsCollector.Factory analyticsCollectorFactory,
Clock clock) {
componentListener = new ComponentListener();
videoListeners = new CopyOnWriteArraySet<>();
@@ -113,9 +162,15 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
videoDebugListeners = new CopyOnWriteArraySet<>();
audioDebugListeners = new CopyOnWriteArraySet<>();
Looper eventLooper = Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper();
- Handler eventHandler = new Handler(eventLooper);
- renderers = renderersFactory.createRenderers(eventHandler, componentListener, componentListener,
- componentListener, componentListener);
+ eventHandler = new Handler(eventLooper);
+ renderers =
+ renderersFactory.createRenderers(
+ eventHandler,
+ componentListener,
+ componentListener,
+ componentListener,
+ componentListener,
+ drmSessionManager);
// Set initial values.
audioVolume = 1;
@@ -125,6 +180,14 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
// Build the player and associated objects.
player = createExoPlayerImpl(renderers, trackSelector, loadControl, clock);
+ analyticsCollector = analyticsCollectorFactory.createAnalyticsCollector(player, clock);
+ addListener(analyticsCollector);
+ videoDebugListeners.add(analyticsCollector);
+ audioDebugListeners.add(analyticsCollector);
+ addMetadataOutput(analyticsCollector);
+ if (drmSessionManager instanceof DefaultDrmSessionManager) {
+ ((DefaultDrmSessionManager) drmSessionManager).addListener(eventHandler, analyticsCollector);
+ }
}
@Override
@@ -267,6 +330,29 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
return Util.getStreamTypeForAudioUsage(audioAttributes.usage);
}
+ /** Returns the {@link AnalyticsCollector} used for collecting analytics events. */
+ public AnalyticsCollector getAnalyticsCollector() {
+ return analyticsCollector;
+ }
+
+ /**
+ * Adds an {@link AnalyticsListener} to receive analytics events.
+ *
+ * @param listener The listener to be added.
+ */
+ public void addAnalyticsListener(AnalyticsListener listener) {
+ analyticsCollector.addListener(listener);
+ }
+
+ /**
+ * Removes an {@link AnalyticsListener}.
+ *
+ * @param listener The listener to be removed.
+ */
+ public void removeAnalyticsListener(AnalyticsListener listener) {
+ analyticsCollector.removeListener(listener);
+ }
+
/**
* Sets the attributes for audio playback, used by the underlying audio track. If not set, the
* default audio attributes will be used. They are suitable for general media playback.
@@ -449,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);
}
@@ -465,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);
}
@@ -483,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);
}
@@ -568,13 +660,26 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
return player.getPlaybackState();
}
+ @Override
+ public ExoPlaybackException getPlaybackError() {
+ return player.getPlaybackError();
+ }
+
@Override
public void prepare(MediaSource mediaSource) {
- player.prepare(mediaSource);
+ prepare(mediaSource, /* resetPosition= */ true, /* resetState= */ true);
}
@Override
public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
+ if (this.mediaSource != mediaSource) {
+ if (this.mediaSource != null) {
+ this.mediaSource.removeEventListener(analyticsCollector);
+ analyticsCollector.resetForNewMediaSource();
+ }
+ mediaSource.addEventListener(eventHandler, analyticsCollector);
+ this.mediaSource = mediaSource;
+ }
player.prepare(mediaSource, resetPosition, resetState);
}
@@ -615,21 +720,25 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
@Override
public void seekToDefaultPosition() {
+ analyticsCollector.notifySeekStarted();
player.seekToDefaultPosition();
}
@Override
public void seekToDefaultPosition(int windowIndex) {
+ analyticsCollector.notifySeekStarted();
player.seekToDefaultPosition(windowIndex);
}
@Override
public void seekTo(long positionMs) {
+ analyticsCollector.notifySeekStarted();
player.seekTo(positionMs);
}
@Override
public void seekTo(int windowIndex, long positionMs) {
+ analyticsCollector.notifySeekStarted();
player.seekTo(windowIndex, positionMs);
}
@@ -648,14 +757,24 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
player.setSeekParameters(seekParameters);
}
+ @Override
+ public @Nullable Object getCurrentTag() {
+ return player.getCurrentTag();
+ }
+
@Override
public void stop() {
- player.stop();
+ stop(/* reset= */ false);
}
@Override
public void stop(boolean reset) {
player.stop(reset);
+ if (mediaSource != null) {
+ mediaSource.removeEventListener(analyticsCollector);
+ mediaSource = null;
+ analyticsCollector.resetForNewMediaSource();
+ }
}
@Override
@@ -668,6 +787,9 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
}
surface = null;
}
+ if (mediaSource != null) {
+ mediaSource.removeEventListener(analyticsCollector);
+ }
}
@Override
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java
index 50a3e66880..600fbc3014 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2;
+import android.support.annotation.Nullable;
import android.util.Pair;
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
import com.google.android.exoplayer2.util.Assertions;
@@ -118,10 +119,8 @@ public abstract class Timeline {
*/
public static final class Window {
- /**
- * An identifier for the window. Not necessarily unique.
- */
- public Object id;
+ /** A tag for the window. Not necessarily unique. */
+ public @Nullable Object tag;
/**
* The start time of the presentation to which this window belongs in milliseconds since the
@@ -174,13 +173,19 @@ public abstract class Timeline {
*/
public long positionInFirstPeriodUs;
- /**
- * Sets the data held by this window.
- */
- public Window set(Object id, long presentationStartTimeMs, long windowStartTimeMs,
- boolean isSeekable, boolean isDynamic, long defaultPositionUs, long durationUs,
- int firstPeriodIndex, int lastPeriodIndex, long positionInFirstPeriodUs) {
- this.id = id;
+ /** Sets the data held by this window. */
+ public Window set(
+ @Nullable Object tag,
+ long presentationStartTimeMs,
+ long windowStartTimeMs,
+ boolean isSeekable,
+ boolean isDynamic,
+ long defaultPositionUs,
+ long durationUs,
+ int firstPeriodIndex,
+ int lastPeriodIndex,
+ long positionInFirstPeriodUs) {
+ this.tag = tag;
this.presentationStartTimeMs = presentationStartTimeMs;
this.windowStartTimeMs = windowStartTimeMs;
this.isSeekable = isSeekable;
@@ -486,38 +491,36 @@ public abstract class Timeline {
}
- /**
- * An empty timeline.
- */
- public static final Timeline EMPTY = new Timeline() {
+ /** An empty timeline. */
+ public static final Timeline EMPTY =
+ new Timeline() {
- @Override
- public int getWindowCount() {
- return 0;
- }
+ @Override
+ public int getWindowCount() {
+ return 0;
+ }
- @Override
- public Window getWindow(int windowIndex, Window window, boolean setIds,
- long defaultPositionProjectionUs) {
- throw new IndexOutOfBoundsException();
- }
+ @Override
+ public Window getWindow(
+ int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) {
+ throw new IndexOutOfBoundsException();
+ }
- @Override
- public int getPeriodCount() {
- return 0;
- }
+ @Override
+ public int getPeriodCount() {
+ return 0;
+ }
- @Override
- public Period getPeriod(int periodIndex, Period period, boolean setIds) {
- throw new IndexOutOfBoundsException();
- }
+ @Override
+ public Period getPeriod(int periodIndex, Period period, boolean setIds) {
+ throw new IndexOutOfBoundsException();
+ }
- @Override
- public int getIndexOfPeriod(Object uid) {
- return C.INDEX_UNSET;
- }
-
- };
+ @Override
+ public int getIndexOfPeriod(Object uid) {
+ return C.INDEX_UNSET;
+ }
+ };
/**
* Returns whether the timeline is empty.
@@ -607,7 +610,7 @@ public abstract class Timeline {
/**
* Populates a {@link Window} with data for the window at the specified index. Does not populate
- * {@link Window#id}.
+ * {@link Window#tag}.
*
* @param windowIndex The index of the window.
* @param window The {@link Window} to populate. Must not be null.
@@ -622,12 +625,12 @@ public abstract class Timeline {
*
* @param windowIndex The index of the window.
* @param window The {@link Window} to populate. Must not be null.
- * @param setIds Whether {@link Window#id} should be populated. If false, the field will be set to
- * null. The caller should pass false for efficiency reasons unless the field is required.
+ * @param setTag Whether {@link Window#tag} should be populated. If false, the field will be set
+ * to null. The caller should pass false for efficiency reasons unless the field is required.
* @return The populated {@link Window}, for convenience.
*/
- public final Window getWindow(int windowIndex, Window window, boolean setIds) {
- return getWindow(windowIndex, window, setIds, 0);
+ public final Window getWindow(int windowIndex, Window window, boolean setTag) {
+ return getWindow(windowIndex, window, setTag, 0);
}
/**
@@ -635,14 +638,14 @@ public abstract class Timeline {
*
* @param windowIndex The index of the window.
* @param window The {@link Window} to populate. Must not be null.
- * @param setIds Whether {@link Window#id} should be populated. If false, the field will be set to
- * null. The caller should pass false for efficiency reasons unless the field is required.
+ * @param setTag Whether {@link Window#tag} should be populated. If false, the field will be set
+ * to null. The caller should pass false for efficiency reasons unless the field is required.
* @param defaultPositionProjectionUs A duration into the future that the populated window's
* default start position should be projected.
* @return The populated {@link Window}, for convenience.
*/
- public abstract Window getWindow(int windowIndex, Window window, boolean setIds,
- long defaultPositionProjectionUs);
+ public abstract Window getWindow(
+ int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs);
/**
* Returns the number of periods in the timeline.
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
new file mode 100644
index 0000000000..43ef308f27
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java
@@ -0,0 +1,798 @@
+/*
+ * 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.analytics;
+
+import android.net.NetworkInfo;
+import android.support.annotation.Nullable;
+import android.view.Surface;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.PlaybackParameters;
+import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.Timeline.Period;
+import com.google.android.exoplayer2.Timeline.Window;
+import com.google.android.exoplayer2.analytics.AnalyticsListener.EventTime;
+import com.google.android.exoplayer2.audio.AudioRendererEventListener;
+import com.google.android.exoplayer2.decoder.DecoderCounters;
+import com.google.android.exoplayer2.drm.DefaultDrmSessionEventListener;
+import com.google.android.exoplayer2.metadata.Metadata;
+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.trackselection.TrackSelectionArray;
+import com.google.android.exoplayer2.upstream.BandwidthMeter;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Clock;
+import com.google.android.exoplayer2.video.VideoRendererEventListener;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+/**
+ * Data collector which is able to forward analytics events to {@link AnalyticsListener}s by
+ * listening to all available ExoPlayer listeners.
+ */
+public class AnalyticsCollector
+ implements Player.EventListener,
+ MetadataOutput,
+ AudioRendererEventListener,
+ VideoRendererEventListener,
+ MediaSourceEventListener,
+ BandwidthMeter.EventListener,
+ DefaultDrmSessionEventListener {
+
+ /** Factory for an analytics collector. */
+ public static class Factory {
+
+ /**
+ * Creates an analytics collector for the specified player.
+ *
+ * @param player The {@link Player} for which data will be collected.
+ * @param clock A {@link Clock} used to generate timestamps.
+ * @return An analytics collector.
+ */
+ public AnalyticsCollector createAnalyticsCollector(Player player, Clock clock) {
+ return new AnalyticsCollector(player, clock);
+ }
+ }
+
+ private final CopyOnWriteArraySet listeners;
+ private final Player player;
+ private final Clock clock;
+ private final Window window;
+ private final MediaPeriodQueueTracker mediaPeriodQueueTracker;
+
+ /**
+ * Creates an analytics collector for the specified player.
+ *
+ * @param player The {@link Player} for which data will be collected.
+ * @param clock A {@link Clock} used to generate timestamps.
+ */
+ protected AnalyticsCollector(Player player, Clock clock) {
+ this.player = Assertions.checkNotNull(player);
+ this.clock = Assertions.checkNotNull(clock);
+ listeners = new CopyOnWriteArraySet<>();
+ mediaPeriodQueueTracker = new MediaPeriodQueueTracker();
+ window = new Window();
+ }
+
+ /**
+ * Adds a listener for analytics events.
+ *
+ * @param listener The listener to add.
+ */
+ public void addListener(AnalyticsListener listener) {
+ listeners.add(listener);
+ }
+
+ /**
+ * Removes a previously added analytics event listener.
+ *
+ * @param listener The listener to remove.
+ */
+ public void removeListener(AnalyticsListener listener) {
+ listeners.remove(listener);
+ }
+
+ // External events.
+
+ /**
+ * Notify analytics collector that a seek operation will start. Should be called before the player
+ * adjusts its state and position to the seek.
+ */
+ public final void notifySeekStarted() {
+ if (!mediaPeriodQueueTracker.isSeeking()) {
+ EventTime eventTime = generatePlayingMediaPeriodEventTime();
+ mediaPeriodQueueTracker.onSeekStarted();
+ for (AnalyticsListener listener : listeners) {
+ listener.onSeekStarted(eventTime);
+ }
+ }
+ }
+
+ /**
+ * Notify analytics collector that the viewport size changed.
+ *
+ * @param width The new width of the viewport in device-independent pixels (dp).
+ * @param height The new height of the viewport in device-independent pixels (dp).
+ */
+ public final void notifyViewportSizeChanged(int width, int height) {
+ EventTime eventTime = generatePlayingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onViewportSizeChange(eventTime, width, height);
+ }
+ }
+
+ /**
+ * Notify analytics collector that the network type or connectivity changed.
+ *
+ * @param networkInfo The new network info, or null if no network connection exists.
+ */
+ public final void notifyNetworkTypeChanged(@Nullable NetworkInfo networkInfo) {
+ EventTime eventTime = generatePlayingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onNetworkTypeChanged(eventTime, networkInfo);
+ }
+ }
+
+ /**
+ * Resets the analytics collector for a new media source. Should be called before the player is
+ * prepared with a new media source.
+ */
+ public final void resetForNewMediaSource() {
+ // Copying the list is needed because onMediaPeriodReleased will modify the list.
+ List activeMediaPeriods =
+ new ArrayList<>(mediaPeriodQueueTracker.activeMediaPeriods);
+ for (WindowAndMediaPeriodId mediaPeriod : activeMediaPeriods) {
+ onMediaPeriodReleased(mediaPeriod.windowIndex, mediaPeriod.mediaPeriodId);
+ }
+ }
+
+ // MetadataOutput implementation.
+
+ @Override
+ public final void onMetadata(Metadata metadata) {
+ EventTime eventTime = generatePlayingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onMetadata(eventTime, metadata);
+ }
+ }
+
+ // AudioRendererEventListener implementation.
+
+ @Override
+ public final void onAudioEnabled(DecoderCounters counters) {
+ // The renderers are only enabled after we changed the playing media period.
+ EventTime eventTime = generatePlayingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onDecoderEnabled(eventTime, C.TRACK_TYPE_AUDIO, counters);
+ }
+ }
+
+ @Override
+ public final void onAudioSessionId(int audioSessionId) {
+ EventTime eventTime = generateReadingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onAudioSessionId(eventTime, audioSessionId);
+ }
+ }
+
+ @Override
+ public final void onAudioDecoderInitialized(
+ String decoderName, long initializedTimestampMs, long initializationDurationMs) {
+ EventTime eventTime = generateReadingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onDecoderInitialized(
+ eventTime, C.TRACK_TYPE_AUDIO, decoderName, initializationDurationMs);
+ }
+ }
+
+ @Override
+ public final void onAudioInputFormatChanged(Format format) {
+ EventTime eventTime = generateReadingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onDecoderInputFormatChanged(eventTime, C.TRACK_TYPE_AUDIO, format);
+ }
+ }
+
+ @Override
+ public final void onAudioSinkUnderrun(
+ int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
+ EventTime eventTime = generateReadingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onAudioUnderrun(eventTime, bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
+ }
+ }
+
+ @Override
+ public final void onAudioDisabled(DecoderCounters counters) {
+ // The renderers are disabled after we changed the playing media period on the playback thread
+ // but before this change is reported to the app thread.
+ EventTime eventTime = generateLastReportedPlayingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onDecoderDisabled(eventTime, C.TRACK_TYPE_AUDIO, counters);
+ }
+ }
+
+ // VideoRendererEventListener implementation.
+
+ @Override
+ public final void onVideoEnabled(DecoderCounters counters) {
+ // The renderers are only enabled after we changed the playing media period.
+ EventTime eventTime = generatePlayingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onDecoderEnabled(eventTime, C.TRACK_TYPE_VIDEO, counters);
+ }
+ }
+
+ @Override
+ public final void onVideoDecoderInitialized(
+ String decoderName, long initializedTimestampMs, long initializationDurationMs) {
+ EventTime eventTime = generateReadingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onDecoderInitialized(
+ eventTime, C.TRACK_TYPE_VIDEO, decoderName, initializationDurationMs);
+ }
+ }
+
+ @Override
+ public final void onVideoInputFormatChanged(Format format) {
+ EventTime eventTime = generateReadingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onDecoderInputFormatChanged(eventTime, C.TRACK_TYPE_VIDEO, format);
+ }
+ }
+
+ @Override
+ public final void onDroppedFrames(int count, long elapsedMs) {
+ EventTime eventTime = generateLastReportedPlayingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onDroppedVideoFrames(eventTime, count, elapsedMs);
+ }
+ }
+
+ @Override
+ public final void onVideoSizeChanged(
+ int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {
+ EventTime eventTime = generateReadingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onVideoSizeChanged(
+ eventTime, width, height, unappliedRotationDegrees, pixelWidthHeightRatio);
+ }
+ }
+
+ @Override
+ public final void onRenderedFirstFrame(Surface surface) {
+ EventTime eventTime = generateReadingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onRenderedFirstFrame(eventTime, surface);
+ }
+ }
+
+ @Override
+ public final void onVideoDisabled(DecoderCounters counters) {
+ // The renderers are disabled after we changed the playing media period on the playback thread
+ // but before this change is reported to the app thread.
+ EventTime eventTime = generateLastReportedPlayingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onDecoderDisabled(eventTime, C.TRACK_TYPE_VIDEO, counters);
+ }
+ }
+
+ // MediaSourceEventListener implementation.
+
+ @Override
+ public final void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) {
+ mediaPeriodQueueTracker.onMediaPeriodCreated(windowIndex, mediaPeriodId);
+ EventTime eventTime = generateEventTime(windowIndex, mediaPeriodId);
+ for (AnalyticsListener listener : listeners) {
+ listener.onMediaPeriodCreated(eventTime);
+ }
+ }
+
+ @Override
+ public final void onMediaPeriodReleased(int windowIndex, MediaPeriodId mediaPeriodId) {
+ mediaPeriodQueueTracker.onMediaPeriodReleased(windowIndex, mediaPeriodId);
+ EventTime eventTime = generateEventTime(windowIndex, mediaPeriodId);
+ for (AnalyticsListener listener : listeners) {
+ listener.onMediaPeriodReleased(eventTime);
+ }
+ }
+
+ @Override
+ public final void onLoadStarted(
+ int windowIndex,
+ @Nullable MediaPeriodId mediaPeriodId,
+ LoadEventInfo loadEventInfo,
+ MediaLoadData mediaLoadData) {
+ EventTime eventTime = generateEventTime(windowIndex, mediaPeriodId);
+ for (AnalyticsListener listener : listeners) {
+ listener.onLoadStarted(eventTime, loadEventInfo, mediaLoadData);
+ }
+ }
+
+ @Override
+ public final void onLoadCompleted(
+ int windowIndex,
+ @Nullable MediaPeriodId mediaPeriodId,
+ LoadEventInfo loadEventInfo,
+ MediaLoadData mediaLoadData) {
+ EventTime eventTime = generateEventTime(windowIndex, mediaPeriodId);
+ for (AnalyticsListener listener : listeners) {
+ listener.onLoadCompleted(eventTime, loadEventInfo, mediaLoadData);
+ }
+ }
+
+ @Override
+ public final void onLoadCanceled(
+ int windowIndex,
+ @Nullable MediaPeriodId mediaPeriodId,
+ LoadEventInfo loadEventInfo,
+ MediaLoadData mediaLoadData) {
+ EventTime eventTime = generateEventTime(windowIndex, mediaPeriodId);
+ for (AnalyticsListener listener : listeners) {
+ listener.onLoadCanceled(eventTime, loadEventInfo, mediaLoadData);
+ }
+ }
+
+ @Override
+ public final void onLoadError(
+ int windowIndex,
+ @Nullable MediaPeriodId mediaPeriodId,
+ LoadEventInfo loadEventInfo,
+ MediaLoadData mediaLoadData,
+ IOException error,
+ boolean wasCanceled) {
+ EventTime eventTime = generateEventTime(windowIndex, mediaPeriodId);
+ for (AnalyticsListener listener : listeners) {
+ listener.onLoadError(eventTime, loadEventInfo, mediaLoadData, error, wasCanceled);
+ }
+ }
+
+ @Override
+ public final void onReadingStarted(int windowIndex, MediaPeriodId mediaPeriodId) {
+ mediaPeriodQueueTracker.onReadingStarted(windowIndex, mediaPeriodId);
+ EventTime eventTime = generateEventTime(windowIndex, mediaPeriodId);
+ for (AnalyticsListener listener : listeners) {
+ listener.onReadingStarted(eventTime);
+ }
+ }
+
+ @Override
+ public final void onUpstreamDiscarded(
+ int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) {
+ EventTime eventTime = generateEventTime(windowIndex, mediaPeriodId);
+ for (AnalyticsListener listener : listeners) {
+ listener.onUpstreamDiscarded(eventTime, mediaLoadData);
+ }
+ }
+
+ @Override
+ public final void onDownstreamFormatChanged(
+ int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) {
+ EventTime eventTime = generateEventTime(windowIndex, mediaPeriodId);
+ for (AnalyticsListener listener : listeners) {
+ listener.onDownstreamFormatChanged(eventTime, mediaLoadData);
+ }
+ }
+
+ // Player.EventListener implementation.
+
+ // TODO: Add onFinishedReportingChanges to Player.EventListener to know when a set of simultaneous
+ // callbacks finished. This helps to assign exactly the same EventTime to all of them instead of
+ // having slightly different real times.
+
+ @Override
+ public final void onTimelineChanged(
+ Timeline timeline, Object manifest, @Player.TimelineChangeReason int reason) {
+ mediaPeriodQueueTracker.onTimelineChanged(timeline);
+ EventTime eventTime = generatePlayingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onTimelineChanged(eventTime, reason);
+ }
+ }
+
+ @Override
+ public final void onTracksChanged(
+ TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
+ EventTime eventTime = generatePlayingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onTracksChanged(eventTime, trackGroups, trackSelections);
+ }
+ }
+
+ @Override
+ public final void onLoadingChanged(boolean isLoading) {
+ EventTime eventTime = generatePlayingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onLoadingChanged(eventTime, isLoading);
+ }
+ }
+
+ @Override
+ public final void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
+ EventTime eventTime = generatePlayingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onPlayerStateChanged(eventTime, playWhenReady, playbackState);
+ }
+ }
+
+ @Override
+ public final void onRepeatModeChanged(@Player.RepeatMode int repeatMode) {
+ EventTime eventTime = generatePlayingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onRepeatModeChanged(eventTime, repeatMode);
+ }
+ }
+
+ @Override
+ public final void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
+ EventTime eventTime = generatePlayingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onShuffleModeChanged(eventTime, shuffleModeEnabled);
+ }
+ }
+
+ @Override
+ public final void onPlayerError(ExoPlaybackException error) {
+ EventTime eventTime = generatePlayingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onPlayerError(eventTime, error);
+ }
+ }
+
+ @Override
+ public final void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
+ mediaPeriodQueueTracker.onPositionDiscontinuity(reason);
+ EventTime eventTime = generatePlayingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onPositionDiscontinuity(eventTime, reason);
+ }
+ }
+
+ @Override
+ public final void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
+ EventTime eventTime = generatePlayingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onPlaybackParametersChanged(eventTime, playbackParameters);
+ }
+ }
+
+ @Override
+ public final void onSeekProcessed() {
+ if (mediaPeriodQueueTracker.isSeeking()) {
+ mediaPeriodQueueTracker.onSeekProcessed();
+ EventTime eventTime = generatePlayingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onSeekProcessed(eventTime);
+ }
+ }
+ }
+
+ // BandwidthMeter.Listener implementation.
+
+ @Override
+ public final void onBandwidthSample(int elapsedMs, long bytes, long bitrate) {
+ EventTime eventTime = generateLoadingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onBandwidthEstimate(eventTime, elapsedMs, bytes, bitrate);
+ }
+ }
+
+ // DefaultDrmSessionManager.EventListener implementation.
+
+ @Override
+ public final void onDrmKeysLoaded() {
+ EventTime eventTime = generateReadingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onDrmKeysLoaded(eventTime);
+ }
+ }
+
+ @Override
+ public final void onDrmSessionManagerError(Exception error) {
+ EventTime eventTime = generateReadingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onDrmSessionManagerError(eventTime, error);
+ }
+ }
+
+ @Override
+ public final void onDrmKeysRestored() {
+ EventTime eventTime = generateReadingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onDrmKeysRestored(eventTime);
+ }
+ }
+
+ @Override
+ public final void onDrmKeysRemoved() {
+ EventTime eventTime = generateReadingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onDrmKeysRemoved(eventTime);
+ }
+ }
+
+ // Internal methods.
+
+ /** Returns read-only set of registered listeners. */
+ protected Set getListeners() {
+ return Collections.unmodifiableSet(listeners);
+ }
+
+ /** Returns a new {@link EventTime} for the specified window index and media period id. */
+ protected EventTime generateEventTime(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {
+ long realtimeMs = clock.elapsedRealtime();
+ Timeline timeline = player.getCurrentTimeline();
+ long eventPositionMs;
+ if (windowIndex == player.getCurrentWindowIndex()) {
+ if (mediaPeriodId != null && mediaPeriodId.isAd()) {
+ // This event is for an ad in the currently playing window.
+ eventPositionMs =
+ player.getCurrentAdGroupIndex() == mediaPeriodId.adGroupIndex
+ && player.getCurrentAdIndexInAdGroup() == mediaPeriodId.adIndexInAdGroup
+ ? player.getCurrentPosition()
+ : 0 /* Assume start position of 0 for a future ad. */;
+ } else {
+ // This event is for content in the currently playing window.
+ eventPositionMs = player.getContentPosition();
+ }
+ } 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;
+ } else {
+ // This event is for content in a future window. Assume default start position.
+ eventPositionMs = timeline.getWindow(windowIndex, window).getDefaultPositionMs();
+ }
+ // TODO(b/30792113): implement this properly (player.getTotalBufferedDuration()).
+ long bufferedDurationMs = player.getBufferedPosition() - player.getContentPosition();
+ return new EventTime(
+ realtimeMs,
+ timeline,
+ windowIndex,
+ mediaPeriodId,
+ eventPositionMs,
+ player.getCurrentPosition(),
+ bufferedDurationMs);
+ }
+
+ private EventTime generateEventTime(@Nullable WindowAndMediaPeriodId mediaPeriod) {
+ if (mediaPeriod == null) {
+ int windowIndex = player.getCurrentWindowIndex();
+ MediaPeriodId mediaPeriodId = mediaPeriodQueueTracker.tryResolveWindowIndex(windowIndex);
+ return generateEventTime(windowIndex, mediaPeriodId);
+ }
+ return generateEventTime(mediaPeriod.windowIndex, mediaPeriod.mediaPeriodId);
+ }
+
+ private EventTime generateLastReportedPlayingMediaPeriodEventTime() {
+ return generateEventTime(mediaPeriodQueueTracker.getLastReportedPlayingMediaPeriod());
+ }
+
+ private EventTime generatePlayingMediaPeriodEventTime() {
+ return generateEventTime(mediaPeriodQueueTracker.getPlayingMediaPeriod());
+ }
+
+ private EventTime generateReadingMediaPeriodEventTime() {
+ return generateEventTime(mediaPeriodQueueTracker.getReadingMediaPeriod());
+ }
+
+ private EventTime generateLoadingMediaPeriodEventTime() {
+ return generateEventTime(mediaPeriodQueueTracker.getLoadingMediaPeriod());
+ }
+
+ /** Keeps track of the active media periods and currently playing and reading media period. */
+ private static final class MediaPeriodQueueTracker {
+
+ // 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 Period period;
+
+ private WindowAndMediaPeriodId lastReportedPlayingMediaPeriod;
+ private WindowAndMediaPeriodId readingMediaPeriod;
+ private Timeline timeline;
+ private boolean isSeeking;
+
+ public MediaPeriodQueueTracker() {
+ activeMediaPeriods = new ArrayList<>();
+ period = new Period();
+ timeline = Timeline.EMPTY;
+ }
+
+ /**
+ * 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 WindowAndMediaPeriodId getPlayingMediaPeriod() {
+ return activeMediaPeriods.isEmpty() || timeline.isEmpty() || isSeeking
+ ? null
+ : activeMediaPeriods.get(0);
+ }
+
+ /**
+ * 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 WindowAndMediaPeriodId getLastReportedPlayingMediaPeriod() {
+ return lastReportedPlayingMediaPeriod;
+ }
+
+ /**
+ * 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 WindowAndMediaPeriodId getReadingMediaPeriod() {
+ return readingMediaPeriod;
+ }
+
+ /**
+ * Returns the {@link MediaPeriodId} of the media period at the end of the queue which is
+ * currently loading or will be the next one loading. May be null, if no media period is active
+ * yet.
+ */
+ public @Nullable WindowAndMediaPeriodId getLoadingMediaPeriod() {
+ return activeMediaPeriods.isEmpty()
+ ? null
+ : activeMediaPeriods.get(activeMediaPeriods.size() - 1);
+ }
+
+ /** Returns whether the player is currently seeking. */
+ public boolean isSeeking() {
+ return isSeeking;
+ }
+
+ /**
+ * 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) {
+ int timelinePeriodCount = timeline.getPeriodCount();
+ for (int i = 0; i < activeMediaPeriods.size(); i++) {
+ 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 = mediaPeriod.mediaPeriodId;
+ }
+ }
+ }
+ return match;
+ }
+
+ /** Updates the queue with a reported position discontinuity . */
+ public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
+ updateLastReportedPlayingMediaPeriod();
+ }
+
+ /** Updates the queue with a reported timeline change. */
+ public void onTimelineChanged(Timeline timeline) {
+ for (int i = 0; i < activeMediaPeriods.size(); i++) {
+ activeMediaPeriods.set(
+ i, updateMediaPeriodToNewTimeline(activeMediaPeriods.get(i), timeline));
+ }
+ if (readingMediaPeriod != null) {
+ readingMediaPeriod = updateMediaPeriodToNewTimeline(readingMediaPeriod, timeline);
+ }
+ this.timeline = timeline;
+ updateLastReportedPlayingMediaPeriod();
+ }
+
+ /** Updates the queue with a reported start of seek. */
+ public void onSeekStarted() {
+ isSeeking = true;
+ }
+
+ /** Updates the queue with a reported processed seek. */
+ public void onSeekProcessed() {
+ isSeeking = false;
+ updateLastReportedPlayingMediaPeriod();
+ }
+
+ /** Updates the queue with a newly created media period. */
+ 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(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(int windowIndex, MediaPeriodId mediaPeriodId) {
+ readingMediaPeriod = new WindowAndMediaPeriodId(windowIndex, mediaPeriodId);
+ }
+
+ private void updateLastReportedPlayingMediaPeriod() {
+ if (!activeMediaPeriods.isEmpty()) {
+ lastReportedPlayingMediaPeriod = activeMediaPeriods.get(0);
+ }
+ }
+
+ private WindowAndMediaPeriodId updateMediaPeriodToNewTimeline(
+ WindowAndMediaPeriodId mediaPeriod, Timeline newTimeline) {
+ if (newTimeline.isEmpty() || timeline.isEmpty()) {
+ return mediaPeriod;
+ }
+ 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
new file mode 100644
index 0000000000..48057f2bff
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java
@@ -0,0 +1,465 @@
+/*
+ * 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.analytics;
+
+import android.net.NetworkInfo;
+import android.support.annotation.Nullable;
+import android.view.Surface;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.PlaybackParameters;
+import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.Player.DiscontinuityReason;
+import com.google.android.exoplayer2.Player.TimelineChangeReason;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.audio.AudioSink;
+import com.google.android.exoplayer2.decoder.DecoderCounters;
+import com.google.android.exoplayer2.metadata.Metadata;
+import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
+import com.google.android.exoplayer2.source.MediaSourceEventListener.LoadEventInfo;
+import com.google.android.exoplayer2.source.MediaSourceEventListener.MediaLoadData;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import java.io.IOException;
+
+/**
+ * A listener for analytics events.
+ *
+ * All events are recorded with an {@link EventTime} specifying the elapsed real time and media
+ * time at the time of the event.
+ */
+public interface AnalyticsListener {
+
+ /** Time information of an event. */
+ final class EventTime {
+
+ /**
+ * Elapsed real-time as returned by {@code SystemClock.elapsedRealtime()} at the time of the
+ * event, in milliseconds.
+ */
+ public final long realtimeMs;
+
+ /** Timeline at the time of the event. */
+ public final Timeline timeline;
+
+ /**
+ * Window index in the {@code timeline} this event belongs to, or the prospective window index
+ * if the timeline is not yet known and empty.
+ */
+ public final int windowIndex;
+
+ /**
+ * Media period identifier for the media period this event belongs to, or {@code null} if the
+ * event is not associated with a specific media period.
+ */
+ public final @Nullable MediaPeriodId mediaPeriodId;
+
+ /**
+ * Position in the window or ad this event belongs to at the time of the event, in milliseconds.
+ */
+ public final long eventPlaybackPositionMs;
+
+ /**
+ * Position in the current timeline window ({@code timeline.getCurrentWindowIndex()} or the
+ * currently playing ad at the time of the event, in milliseconds.
+ */
+ public final long currentPlaybackPositionMs;
+
+ /**
+ * Total buffered duration from {@link #currentPlaybackPositionMs} at the time of the event, in
+ * milliseconds. This includes pre-buffered data for subsequent ads and windows.
+ */
+ public final long totalBufferedDurationMs;
+
+ /**
+ * @param realtimeMs Elapsed real-time as returned by {@code SystemClock.elapsedRealtime()} at
+ * the time of the event, in milliseconds.
+ * @param timeline Timeline at the time of the event.
+ * @param windowIndex Window index in the {@code timeline} this event belongs to, or the
+ * prospective window index if the timeline is not yet known and empty.
+ * @param mediaPeriodId Media period identifier for the media period this event belongs to, or
+ * {@code null} if the event is not associated with a specific media period.
+ * @param eventPlaybackPositionMs Position in the window or ad this event belongs to at the time
+ * of the event, in milliseconds.
+ * @param currentPlaybackPositionMs Position in the current timeline window ({@code
+ * timeline.getCurrentWindowIndex()} or the currently playing ad at the time of the event,
+ * in milliseconds.
+ * @param totalBufferedDurationMs Total buffered duration from {@link
+ * #currentPlaybackPositionMs} at the time of the event, in milliseconds. This includes
+ * pre-buffered data for subsequent ads and windows.
+ */
+ public EventTime(
+ long realtimeMs,
+ Timeline timeline,
+ int windowIndex,
+ @Nullable MediaPeriodId mediaPeriodId,
+ long eventPlaybackPositionMs,
+ long currentPlaybackPositionMs,
+ long totalBufferedDurationMs) {
+ this.realtimeMs = realtimeMs;
+ this.timeline = timeline;
+ this.windowIndex = windowIndex;
+ this.mediaPeriodId = mediaPeriodId;
+ this.eventPlaybackPositionMs = eventPlaybackPositionMs;
+ this.currentPlaybackPositionMs = currentPlaybackPositionMs;
+ this.totalBufferedDurationMs = totalBufferedDurationMs;
+ }
+ }
+
+ /**
+ * Called when the player state changed.
+ *
+ * @param eventTime The event time.
+ * @param playWhenReady Whether the playback will proceed when ready.
+ * @param playbackState One of the {@link Player}.STATE constants.
+ */
+ void onPlayerStateChanged(EventTime eventTime, boolean playWhenReady, int playbackState);
+
+ /**
+ * Called when the timeline changed.
+ *
+ * @param eventTime The event time.
+ * @param reason The reason for the timeline change.
+ */
+ void onTimelineChanged(EventTime eventTime, @TimelineChangeReason int reason);
+
+ /**
+ * Called when a position discontinuity occurred.
+ *
+ * @param eventTime The event time.
+ * @param reason The reason for the position discontinuity.
+ */
+ void onPositionDiscontinuity(EventTime eventTime, @DiscontinuityReason int reason);
+
+ /**
+ * Called when a seek operation started.
+ *
+ * @param eventTime The event time.
+ */
+ void onSeekStarted(EventTime eventTime);
+
+ /**
+ * Called when a seek operation was processed.
+ *
+ * @param eventTime The event time.
+ */
+ void onSeekProcessed(EventTime eventTime);
+
+ /**
+ * Called when the playback parameters changed.
+ *
+ * @param eventTime The event time.
+ * @param playbackParameters The new playback parameters.
+ */
+ void onPlaybackParametersChanged(EventTime eventTime, PlaybackParameters playbackParameters);
+
+ /**
+ * Called when the repeat mode changed.
+ *
+ * @param eventTime The event time.
+ * @param repeatMode The new repeat mode.
+ */
+ void onRepeatModeChanged(EventTime eventTime, @Player.RepeatMode int repeatMode);
+
+ /**
+ * Called when the shuffle mode changed.
+ *
+ * @param eventTime The event time.
+ * @param shuffleModeEnabled Whether the shuffle mode is enabled.
+ */
+ void onShuffleModeChanged(EventTime eventTime, boolean shuffleModeEnabled);
+
+ /**
+ * Called when the player starts or stops loading data from a source.
+ *
+ * @param eventTime The event time.
+ * @param isLoading Whether the player is loading.
+ */
+ void onLoadingChanged(EventTime eventTime, boolean isLoading);
+
+ /**
+ * Called when a fatal player error occurred.
+ *
+ * @param eventTime The event time.
+ * @param error The error.
+ */
+ void onPlayerError(EventTime eventTime, ExoPlaybackException error);
+
+ /**
+ * Called when the available or selected tracks for the renderers changed.
+ *
+ * @param eventTime The event time.
+ * @param trackGroups The available tracks. May be empty.
+ * @param trackSelections The track selections for each renderer. May contain null elements.
+ */
+ void onTracksChanged(
+ EventTime eventTime, TrackGroupArray trackGroups, TrackSelectionArray trackSelections);
+
+ /**
+ * Called when a media source started loading data.
+ *
+ * @param eventTime The event time.
+ * @param loadEventInfo The {@link LoadEventInfo} defining the load event.
+ * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded.
+ */
+ void onLoadStarted(EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData);
+
+ /**
+ * Called when a media source completed loading data.
+ *
+ * @param eventTime The event time.
+ * @param loadEventInfo The {@link LoadEventInfo} defining the load event.
+ * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded.
+ */
+ void onLoadCompleted(
+ EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData);
+
+ /**
+ * Called when a media source canceled loading data.
+ *
+ * @param eventTime The event time.
+ * @param loadEventInfo The {@link LoadEventInfo} defining the load event.
+ * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded.
+ */
+ void onLoadCanceled(
+ EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData);
+
+ /**
+ * Called when a media source loading error occurred. These errors are just for informational
+ * purposes and the player may recover.
+ *
+ * @param eventTime The event time.
+ * @param loadEventInfo The {@link LoadEventInfo} defining the load event.
+ * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded.
+ * @param error The load error.
+ * @param wasCanceled Whether the load was canceled as a result of the error.
+ */
+ void onLoadError(
+ EventTime eventTime,
+ LoadEventInfo loadEventInfo,
+ MediaLoadData mediaLoadData,
+ IOException error,
+ boolean wasCanceled);
+
+ /**
+ * Called when the downstream format sent to the renderers changed.
+ *
+ * @param eventTime The event time.
+ * @param mediaLoadData The {@link MediaLoadData} defining the newly selected media data.
+ */
+ void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData);
+
+ /**
+ * Called when data is removed from the back of a media buffer, typically so that it can be
+ * re-buffered in a different format.
+ *
+ * @param eventTime The event time.
+ * @param mediaLoadData The {@link MediaLoadData} defining the media being discarded.
+ */
+ void onUpstreamDiscarded(EventTime eventTime, MediaLoadData mediaLoadData);
+
+ /**
+ * Called when a media source created a media period.
+ *
+ * @param eventTime The event time.
+ */
+ void onMediaPeriodCreated(EventTime eventTime);
+
+ /**
+ * Called when a media source released a media period.
+ *
+ * @param eventTime The event time.
+ */
+ void onMediaPeriodReleased(EventTime eventTime);
+
+ /**
+ * Called when the player started reading a media period.
+ *
+ * @param eventTime The event time.
+ */
+ void onReadingStarted(EventTime eventTime);
+
+ /**
+ * Called when the bandwidth estimate for the current data source has been updated.
+ *
+ * @param eventTime The event time.
+ * @param totalLoadTimeMs The total time spend loading this update is based on, in milliseconds.
+ * @param totalBytesLoaded The total bytes loaded this update is based on.
+ * @param bitrateEstimate The bandwidth estimate, in bits per second.
+ */
+ void onBandwidthEstimate(
+ EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate);
+
+ /**
+ * Called when the viewport size of the output surface changed.
+ *
+ * @param eventTime The event time.
+ * @param width The width of the viewport in device-independent pixels (dp).
+ * @param height The height of the viewport in device-independent pixels (dp).
+ */
+ void onViewportSizeChange(EventTime eventTime, int width, int height);
+
+ /**
+ * Called when the type of the network connection changed.
+ *
+ * @param eventTime The event time.
+ * @param networkInfo The network info for the current connection, or null if disconnected.
+ */
+ void onNetworkTypeChanged(EventTime eventTime, @Nullable NetworkInfo networkInfo);
+
+ /**
+ * Called when there is {@link Metadata} associated with the current playback time.
+ *
+ * @param eventTime The event time.
+ * @param metadata The metadata.
+ */
+ void onMetadata(EventTime eventTime, Metadata metadata);
+
+ /**
+ * Called when an audio or video decoder has been enabled.
+ *
+ * @param eventTime The event time.
+ * @param trackType The track type of the enabled decoder. Either {@link C#TRACK_TYPE_AUDIO} or
+ * {@link C#TRACK_TYPE_VIDEO}.
+ * @param decoderCounters The accumulated event counters associated with this decoder.
+ */
+ void onDecoderEnabled(EventTime eventTime, int trackType, DecoderCounters decoderCounters);
+
+ /**
+ * Called when an audio or video decoder has been initialized.
+ *
+ * @param eventTime The event time.
+ * @param trackType The track type of the initialized decoder. Either {@link C#TRACK_TYPE_AUDIO}
+ * or {@link C#TRACK_TYPE_VIDEO}.
+ * @param decoderName The decoder that was created.
+ * @param initializationDurationMs Time taken to initialize the decoder, in milliseconds.
+ */
+ void onDecoderInitialized(
+ EventTime eventTime, int trackType, String decoderName, long initializationDurationMs);
+
+ /**
+ * Called when an audio or video decoder input format changed.
+ *
+ * @param eventTime The event time.
+ * @param trackType The track type of the decoder whose format changed. Either {@link
+ * C#TRACK_TYPE_AUDIO} or {@link C#TRACK_TYPE_VIDEO}.
+ * @param format The new input format for the decoder.
+ */
+ void onDecoderInputFormatChanged(EventTime eventTime, int trackType, Format format);
+
+ /**
+ * Called when an audio or video decoder has been disabled.
+ *
+ * @param eventTime The event time.
+ * @param trackType The track type of the disabled decoder. Either {@link C#TRACK_TYPE_AUDIO} or
+ * {@link C#TRACK_TYPE_VIDEO}.
+ * @param decoderCounters The accumulated event counters associated with this decoder.
+ */
+ void onDecoderDisabled(EventTime eventTime, int trackType, DecoderCounters decoderCounters);
+
+ /**
+ * Called when the audio session id is set.
+ *
+ * @param eventTime The event time.
+ * @param audioSessionId The audio session id.
+ */
+ void onAudioSessionId(EventTime eventTime, int audioSessionId);
+
+ /**
+ * Called when an audio underrun occurred.
+ *
+ * @param eventTime The event time.
+ * @param bufferSize The size of the {@link AudioSink}'s buffer, in bytes.
+ * @param bufferSizeMs The size of the {@link AudioSink}'s buffer, in milliseconds, if it is
+ * configured for PCM output. {@link C#TIME_UNSET} if it is configured for passthrough output,
+ * as the buffered media can have a variable bitrate so the duration may be unknown.
+ * @param elapsedSinceLastFeedMs The time since the {@link AudioSink} was last fed data.
+ */
+ void onAudioUnderrun(
+ EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs);
+
+ /**
+ * Called after video frames have been dropped.
+ *
+ * @param eventTime The event time.
+ * @param droppedFrames The number of dropped frames since the last call to this method.
+ * @param elapsedMs The duration in milliseconds over which the frames were dropped. This duration
+ * is timed from when the renderer was started or from when dropped frames were last reported
+ * (whichever was more recent), and not from when the first of the reported drops occurred.
+ */
+ void onDroppedVideoFrames(EventTime eventTime, int droppedFrames, long elapsedMs);
+
+ /**
+ * Called before a frame is rendered for the first time since setting the surface, and each time
+ * there's a change in the size or pixel aspect ratio of the video being rendered.
+ *
+ * @param eventTime The event time.
+ * @param width The width of the video.
+ * @param height The height of the video.
+ * @param unappliedRotationDegrees For videos that require a rotation, this is the clockwise
+ * rotation in degrees that the application should apply for the video for it to be rendered
+ * in the correct orientation. This value will always be zero on API levels 21 and above,
+ * since the renderer will apply all necessary rotations internally.
+ * @param pixelWidthHeightRatio The width to height ratio of each pixel.
+ */
+ void onVideoSizeChanged(
+ EventTime eventTime,
+ int width,
+ int height,
+ int unappliedRotationDegrees,
+ float pixelWidthHeightRatio);
+
+ /**
+ * Called when a frame is rendered for the first time since setting the surface, and when a frame
+ * is rendered for the first time since the renderer was reset.
+ *
+ * @param eventTime The event time.
+ * @param surface The {@link Surface} to which a first frame has been rendered, or {@code null} if
+ * the renderer renders to something that isn't a {@link Surface}.
+ */
+ void onRenderedFirstFrame(EventTime eventTime, Surface surface);
+
+ /**
+ * Called each time drm keys are loaded.
+ *
+ * @param eventTime The event time.
+ */
+ void onDrmKeysLoaded(EventTime eventTime);
+
+ /**
+ * Called when a drm error occurs. These errors are just for informational purposes and the player
+ * may recover.
+ *
+ * @param eventTime The event time.
+ * @param error The error.
+ */
+ void onDrmSessionManagerError(EventTime eventTime, Exception error);
+
+ /**
+ * Called each time offline drm keys are restored.
+ *
+ * @param eventTime The event time.
+ */
+ void onDrmKeysRestored(EventTime eventTime);
+
+ /**
+ * Called each time offline drm keys are removed.
+ *
+ * @param eventTime The event time.
+ */
+ void onDrmKeysRemoved(EventTime eventTime);
+}
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
new file mode 100644
index 0000000000..4a49de56b0
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultAnalyticsListener.java
@@ -0,0 +1,166 @@
+/*
+ * 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.analytics;
+
+import android.net.NetworkInfo;
+import android.view.Surface;
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.PlaybackParameters;
+import com.google.android.exoplayer2.decoder.DecoderCounters;
+import com.google.android.exoplayer2.metadata.Metadata;
+import com.google.android.exoplayer2.source.MediaSourceEventListener.LoadEventInfo;
+import com.google.android.exoplayer2.source.MediaSourceEventListener.MediaLoadData;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import java.io.IOException;
+
+/**
+ * {@link AnalyticsListener} allowing selective overrides. All methods are implemented as no-ops.
+ */
+public abstract class DefaultAnalyticsListener implements AnalyticsListener {
+
+ @Override
+ public void onPlayerStateChanged(EventTime eventTime, boolean playWhenReady, int playbackState) {}
+
+ @Override
+ public void onTimelineChanged(EventTime eventTime, int reason) {}
+
+ @Override
+ public void onPositionDiscontinuity(EventTime eventTime, int reason) {}
+
+ @Override
+ public void onSeekStarted(EventTime eventTime) {}
+
+ @Override
+ public void onSeekProcessed(EventTime eventTime) {}
+
+ @Override
+ public void onPlaybackParametersChanged(
+ EventTime eventTime, PlaybackParameters playbackParameters) {}
+
+ @Override
+ public void onRepeatModeChanged(EventTime eventTime, int repeatMode) {}
+
+ @Override
+ public void onShuffleModeChanged(EventTime eventTime, boolean shuffleModeEnabled) {}
+
+ @Override
+ public void onLoadingChanged(EventTime eventTime, boolean isLoading) {}
+
+ @Override
+ public void onPlayerError(EventTime eventTime, ExoPlaybackException error) {}
+
+ @Override
+ public void onTracksChanged(
+ EventTime eventTime, TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {}
+
+ @Override
+ public void onLoadStarted(
+ EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {}
+
+ @Override
+ public void onLoadCompleted(
+ EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {}
+
+ @Override
+ public void onLoadCanceled(
+ EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {}
+
+ @Override
+ public void onLoadError(
+ EventTime eventTime,
+ LoadEventInfo loadEventInfo,
+ MediaLoadData mediaLoadData,
+ IOException error,
+ boolean wasCanceled) {}
+
+ @Override
+ public void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData) {}
+
+ @Override
+ public void onUpstreamDiscarded(EventTime eventTime, MediaLoadData mediaLoadData) {}
+
+ @Override
+ public void onMediaPeriodCreated(EventTime eventTime) {}
+
+ @Override
+ public void onMediaPeriodReleased(EventTime eventTime) {}
+
+ @Override
+ public void onReadingStarted(EventTime eventTime) {}
+
+ @Override
+ public void onBandwidthEstimate(
+ EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate) {}
+
+ @Override
+ public void onViewportSizeChange(EventTime eventTime, int width, int height) {}
+
+ @Override
+ public void onNetworkTypeChanged(EventTime eventTime, NetworkInfo networkInfo) {}
+
+ @Override
+ public void onMetadata(EventTime eventTime, Metadata metadata) {}
+
+ @Override
+ public void onDecoderEnabled(
+ EventTime eventTime, int trackType, DecoderCounters decoderCounters) {}
+
+ @Override
+ public void onDecoderInitialized(
+ EventTime eventTime, int trackType, String decoderName, long initializationDurationMs) {}
+
+ @Override
+ public void onDecoderInputFormatChanged(EventTime eventTime, int trackType, Format format) {}
+
+ @Override
+ public void onDecoderDisabled(
+ EventTime eventTime, int trackType, DecoderCounters decoderCounters) {}
+
+ @Override
+ public void onAudioSessionId(EventTime eventTime, int audioSessionId) {}
+
+ @Override
+ public void onAudioUnderrun(
+ EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {}
+
+ @Override
+ public void onDroppedVideoFrames(EventTime eventTime, int droppedFrames, long elapsedMs) {}
+
+ @Override
+ public void onVideoSizeChanged(
+ EventTime eventTime,
+ int width,
+ int height,
+ int unappliedRotationDegrees,
+ float pixelWidthHeightRatio) {}
+
+ @Override
+ public void onRenderedFirstFrame(EventTime eventTime, Surface surface) {}
+
+ @Override
+ public void onDrmKeysLoaded(EventTime eventTime) {}
+
+ @Override
+ public void onDrmSessionManagerError(EventTime eventTime, Exception error) {}
+
+ @Override
+ public void onDrmKeysRestored(EventTime eventTime) {}
+
+ @Override
+ public void onDrmKeysRemoved(EventTime eventTime) {}
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java
index f45a6a11c6..c61b8ff24c 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java
@@ -15,41 +15,35 @@
*/
package com.google.android.exoplayer2.audio;
-import static com.google.android.exoplayer2.audio.Ac3Util.Ac3SyncFrameInfo.STREAM_TYPE_TYPE0;
-import static com.google.android.exoplayer2.audio.Ac3Util.Ac3SyncFrameInfo.STREAM_TYPE_TYPE1;
-import static com.google.android.exoplayer2.audio.Ac3Util.Ac3SyncFrameInfo.STREAM_TYPE_UNDEFINED;
-
+import android.support.annotation.IntDef;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.audio.Ac3Util.SyncFrameInfo.StreamType;
import com.google.android.exoplayer2.drm.DrmInitData;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableBitArray;
import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.nio.ByteBuffer;
/** Utility methods for parsing Dolby TrueHD and (E-)AC3 syncframes. */
public final class Ac3Util {
- /**
- * Holds sample format information as presented by a syncframe header.
- */
- public static final class Ac3SyncFrameInfo {
+ /** Holds sample format information as presented by a syncframe header. */
+ public static final class SyncFrameInfo {
- /**
- * Undefined AC3 stream type.
- */
+ /** AC3 stream types. See also ETSI TS 102 366 E.1.3.1.1. */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({STREAM_TYPE_UNDEFINED, STREAM_TYPE_TYPE0, STREAM_TYPE_TYPE1, STREAM_TYPE_TYPE2})
+ public @interface StreamType {}
+ /** Undefined AC3 stream type. */
public static final int STREAM_TYPE_UNDEFINED = -1;
- /**
- * Type 0 AC3 stream type. See ETSI TS 102 366 E.1.3.1.1.
- */
+ /** Type 0 AC3 stream type. */
public static final int STREAM_TYPE_TYPE0 = 0;
- /**
- * Type 1 AC3 stream type. See ETSI TS 102 366 E.1.3.1.1.
- */
+ /** Type 1 AC3 stream type. */
public static final int STREAM_TYPE_TYPE1 = 1;
- /**
- * Type 2 AC3 stream type. See ETSI TS 102 366 E.1.3.1.1.
- */
+ /** Type 2 AC3 stream type. */
public static final int STREAM_TYPE_TYPE2 = 2;
/**
@@ -58,10 +52,10 @@ public final class Ac3Util {
*/
public final String mimeType;
/**
- * The type of the stream if {@link #mimeType} is {@link MimeTypes#AUDIO_E_AC3}, or
- * {@link #STREAM_TYPE_UNDEFINED} otherwise.
+ * The type of the stream if {@link #mimeType} is {@link MimeTypes#AUDIO_E_AC3}, or {@link
+ * #STREAM_TYPE_UNDEFINED} otherwise.
*/
- public final int streamType;
+ public final @StreamType int streamType;
/**
* The audio sampling rate in Hz.
*/
@@ -79,8 +73,13 @@ public final class Ac3Util {
*/
public final int sampleCount;
- private Ac3SyncFrameInfo(String mimeType, int streamType, int channelCount, int sampleRate,
- int frameSize, int sampleCount) {
+ private SyncFrameInfo(
+ String mimeType,
+ @StreamType int streamType,
+ int channelCount,
+ int sampleRate,
+ int frameSize,
+ int sampleCount) {
this.mimeType = mimeType;
this.streamType = streamType;
this.channelCount = channelCount;
@@ -96,11 +95,11 @@ public final class Ac3Util {
* of samples extracted from the container corresponding to one syncframe must be an integer
* multiple of this value.
*/
- public static final int TRUEHD_RECHUNK_SAMPLE_COUNT = 8;
+ public static final int TRUEHD_RECHUNK_SAMPLE_COUNT = 16;
/**
* The number of bytes that must be parsed from a TrueHD syncframe to calculate the sample count.
*/
- public static final int TRUEHD_SYNCFRAME_PREFIX_LENGTH = 12;
+ public static final int TRUEHD_SYNCFRAME_PREFIX_LENGTH = 10;
/**
* The number of new samples per (E-)AC-3 audio block.
@@ -212,13 +211,13 @@ public final class Ac3Util {
* @param data The data to parse, positioned at the start of the syncframe.
* @return The (E-)AC-3 format data parsed from the header.
*/
- public static Ac3SyncFrameInfo parseAc3SyncframeInfo(ParsableBitArray data) {
+ public static SyncFrameInfo parseAc3SyncframeInfo(ParsableBitArray data) {
int initialPosition = data.getPosition();
data.skipBits(40);
boolean isEac3 = data.readBits(5) == 16;
data.setPosition(initialPosition);
String mimeType;
- int streamType = STREAM_TYPE_UNDEFINED;
+ @StreamType int streamType = SyncFrameInfo.STREAM_TYPE_UNDEFINED;
int sampleRate;
int acmod;
int frameSize;
@@ -228,7 +227,20 @@ public final class Ac3Util {
if (isEac3) {
// Syntax from ETSI TS 102 366 V1.2.1 subsections E.1.2.1 and E.1.2.2.
data.skipBits(16); // syncword
- streamType = data.readBits(2);
+ switch (data.readBits(2)) { // strmtyp
+ case 0:
+ streamType = SyncFrameInfo.STREAM_TYPE_TYPE0;
+ break;
+ case 1:
+ streamType = SyncFrameInfo.STREAM_TYPE_TYPE1;
+ break;
+ case 2:
+ streamType = SyncFrameInfo.STREAM_TYPE_TYPE2;
+ break;
+ default:
+ streamType = SyncFrameInfo.STREAM_TYPE_UNDEFINED;
+ break;
+ }
data.skipBits(3); // substreamid
frameSize = (data.readBits(11) + 1) * 2;
int fscod = data.readBits(2);
@@ -257,7 +269,7 @@ public final class Ac3Util {
data.skipBits(8); // compr2
}
}
- if (streamType == STREAM_TYPE_TYPE1 && data.readBit()) { // chanmape
+ if (streamType == SyncFrameInfo.STREAM_TYPE_TYPE1 && data.readBit()) { // chanmape
data.skipBits(16); // chanmap
}
if (data.readBit()) { // mixmdate
@@ -273,7 +285,7 @@ public final class Ac3Util {
if (lfeon && data.readBit()) { // lfemixlevcode
data.skipBits(5); // lfemixlevcod
}
- if (streamType == STREAM_TYPE_TYPE0) {
+ if (streamType == SyncFrameInfo.STREAM_TYPE_TYPE0) {
if (data.readBit()) { // pgmscle
data.skipBits(6); //pgmscl
}
@@ -375,10 +387,11 @@ public final class Ac3Util {
data.skipBit(); // sourcefscod
}
}
- if (streamType == 0 && numblkscod != 3) {
+ if (streamType == SyncFrameInfo.STREAM_TYPE_TYPE0 && numblkscod != 3) {
data.skipBit(); // convsync
}
- if (streamType == 2 && (numblkscod == 3 || data.readBit())) { // blkid
+ if (streamType == SyncFrameInfo.STREAM_TYPE_TYPE2
+ && (numblkscod == 3 || data.readBit())) { // blkid
data.skipBits(6); // frmsizecod
}
mimeType = MimeTypes.AUDIO_E_AC3;
@@ -410,8 +423,8 @@ public final class Ac3Util {
lfeon = data.readBit();
channelCount = CHANNEL_COUNT_BY_ACMOD[acmod] + (lfeon ? 1 : 0);
}
- return new Ac3SyncFrameInfo(mimeType, streamType, channelCount, sampleRate, frameSize,
- sampleCount);
+ return new SyncFrameInfo(
+ mimeType, streamType, channelCount, sampleRate, frameSize, sampleCount);
}
/**
@@ -450,6 +463,26 @@ public final class Ac3Util {
: BLOCKS_PER_SYNCFRAME_BY_NUMBLKSCOD[(buffer.get(buffer.position() + 4) & 0x30) >> 4]);
}
+ /**
+ * Returns the offset relative to the buffer's position of the start of a TrueHD syncframe, or
+ * {@link C#INDEX_UNSET} if no syncframe was found. The buffer's position is not modified.
+ *
+ * @param buffer The {@link ByteBuffer} within which to find a syncframe.
+ * @return The offset relative to the buffer's position of the start of a TrueHD syncframe, or
+ * {@link C#INDEX_UNSET} if no syncframe was found.
+ */
+ public static int findTrueHdSyncframeOffset(ByteBuffer buffer) {
+ int startIndex = buffer.position();
+ int endIndex = buffer.limit() - TRUEHD_SYNCFRAME_PREFIX_LENGTH;
+ for (int i = startIndex; i <= endIndex; i++) {
+ // The syncword ends 0xBA for TrueHD or 0xBB for MLP.
+ if ((buffer.getInt(i + 4) & 0xFEFFFFFF) == 0xBA6F72F8) {
+ return i - startIndex;
+ }
+ }
+ return C.INDEX_UNSET;
+ }
+
/**
* Returns the number of audio samples represented by the given TrueHD syncframe, or 0 if the
* buffer is not the start of a syncframe.
@@ -461,30 +494,29 @@ public final class Ac3Util {
*/
public static int parseTrueHdSyncframeAudioSampleCount(byte[] syncframe) {
// TODO: Link to specification if available.
+ // The syncword ends 0xBA for TrueHD or 0xBB for MLP.
if (syncframe[4] != (byte) 0xF8
|| syncframe[5] != (byte) 0x72
|| syncframe[6] != (byte) 0x6F
- || syncframe[7] != (byte) 0xBA) {
+ || (syncframe[7] & 0xFE) != 0xBA) {
return 0;
}
- return 40 << (syncframe[8] & 7);
+ boolean isMlp = (syncframe[7] & 0xFF) == 0xBB;
+ return 40 << ((syncframe[isMlp ? 9 : 8] >> 4) & 0x07);
}
/**
- * Reads the number of audio samples represented by the given TrueHD syncframe, or 0 if the buffer
- * is not the start of a syncframe. The buffer's position is not modified.
+ * Reads the number of audio samples represented by a TrueHD syncframe. The buffer's position is
+ * not modified.
*
- * @param buffer The {@link ByteBuffer} from which to read the syncframe. Must have at least
- * {@link #TRUEHD_SYNCFRAME_PREFIX_LENGTH} bytes remaining.
- * @return The number of audio samples represented by the syncframe, or 0 if the buffer is not the
- * start of a syncframe.
+ * @param buffer The {@link ByteBuffer} from which to read the syncframe.
+ * @param offset The offset of the start of the syncframe relative to the buffer's position.
+ * @return The number of audio samples represented by the syncframe.
*/
- public static int parseTrueHdSyncframeAudioSampleCount(ByteBuffer buffer) {
+ public static int parseTrueHdSyncframeAudioSampleCount(ByteBuffer buffer, int offset) {
// TODO: Link to specification if available.
- if (buffer.getInt(buffer.position() + 4) != 0xBA6F72F8) {
- return 0;
- }
- return 40 << (buffer.get(buffer.position() + 8) & 0x07);
+ boolean isMlp = (buffer.get(buffer.position() + offset + 7) & 0xFF) == 0xBB;
+ return 40 << ((buffer.get(buffer.position() + offset + (isMlp ? 9 : 8)) >> 4) & 0x07);
}
private static int getAc3SyncframeSize(int fscod, int frmsizecod) {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioProcessor.java
index 8a3d624222..f82be31f72 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioProcessor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioProcessor.java
@@ -22,19 +22,20 @@ import java.nio.ByteOrder;
/**
* Interface for audio processors, which take audio data as input and transform it, potentially
* modifying its channel count, encoding and/or sample rate.
- *
- * Call {@link #configure(int, int, int)} to configure the processor to receive input audio, then
- * call {@link #isActive()} to determine whether the processor is active.
- * {@link #queueInput(ByteBuffer)}, {@link #queueEndOfStream()}, {@link #getOutput()},
- * {@link #isEnded()}, {@link #getOutputChannelCount()}, {@link #getOutputEncoding()} and
- * {@link #getOutputSampleRateHz()} may only be called if the processor is active. Call
- * {@link #reset()} to reset the processor to its unconfigured state.
+ *
+ *
Call {@link #configure(int, int, int)} to configure the processor to receive input audio, then
+ * call {@link #isActive()} to determine whether the processor is active. {@link
+ * #queueInput(ByteBuffer)}, {@link #queueEndOfStream()}, {@link #getOutput()}, {@link #isEnded()},
+ * {@link #getOutputChannelCount()}, {@link #getOutputEncoding()} and {@link
+ * #getOutputSampleRateHz()} may only be called if the processor is active. Call {@link #reset()} to
+ * reset the processor to its unconfigured state and release any resources.
+ *
+ *
In addition to being able to modify the format of audio, implementations may allow parameters
+ * to be set that affect the output audio and whether the processor is active/inactive.
*/
public interface AudioProcessor {
- /**
- * Exception thrown when a processor can't be configured for a given input audio format.
- */
+ /** Exception thrown when a processor can't be configured for a given input audio format. */
final class UnhandledFormatException extends Exception {
public UnhandledFormatException(int sampleRateHz, int channelCount, @C.Encoding int encoding) {
@@ -44,33 +45,25 @@ public interface AudioProcessor {
}
- /**
- * An empty, direct {@link ByteBuffer}.
- */
+ /** An empty, direct {@link ByteBuffer}. */
ByteBuffer EMPTY_BUFFER = ByteBuffer.allocateDirect(0).order(ByteOrder.nativeOrder());
/**
- * Configures the processor to process input audio with the specified format. After calling this
- * method, {@link #isActive()} returns whether the processor needs to handle buffers; if not, the
- * processor will not accept any buffers until it is reconfigured. Returns {@code true} if the
- * processor must be flushed, or if the value returned by {@link #isActive()} has changed as a
- * result of the call. If it's active, {@link #getOutputSampleRateHz()},
- * {@link #getOutputChannelCount()} and {@link #getOutputEncoding()} return the processor's output
- * format.
+ * Configures the processor to process input audio with the specified format and returns whether
+ * to {@link #flush()} it. After calling this method, if the processor is active, {@link
+ * #getOutputSampleRateHz()}, {@link #getOutputChannelCount()} and {@link #getOutputEncoding()}
+ * return its output format.
*
* @param sampleRateHz The sample rate of input audio in Hz.
* @param channelCount The number of interleaved channels in input audio.
* @param encoding The encoding of input audio.
- * @return {@code true} if the processor must be flushed or the value returned by
- * {@link #isActive()} has changed as a result of the call.
+ * @return Whether to {@link #flush()} the processor.
* @throws UnhandledFormatException Thrown if the specified format can't be handled as input.
*/
boolean configure(int sampleRateHz, int channelCount, @C.Encoding int encoding)
throws UnhandledFormatException;
- /**
- * Returns whether the processor is configured and active.
- */
+ /** Returns whether the processor is configured and will process input buffers. */
boolean isActive();
/**
@@ -130,14 +123,9 @@ public interface AudioProcessor {
*/
boolean isEnded();
- /**
- * Clears any state in preparation for receiving a new stream of input buffers.
- */
+ /** Clears any state in preparation for receiving a new stream of input buffers. */
void flush();
- /**
- * Resets the processor to its unconfigured state.
- */
+ /** Resets the processor to its unconfigured state. */
void reset();
-
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java
index 6bb5bf7d8e..07584d575e 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java
@@ -192,17 +192,23 @@ public interface AudioSink {
* @param outputChannels A mapping from input to output channels that is applied to this sink's
* input as a preprocessing step, if handling PCM input. Specify {@code null} to leave the
* input unchanged. Otherwise, the element at index {@code i} specifies index of the input
- * channel to map to output channel {@code i} when preprocessing input buffers. After the
- * map is applied the audio data will have {@code outputChannels.length} channels.
- * @param trimStartSamples The number of audio samples to trim from the start of data written to
- * the sink after this call.
- * @param trimEndSamples The number of audio samples to trim from data written to the sink
+ * channel to map to output channel {@code i} when preprocessing input buffers. After the map
+ * is applied the audio data will have {@code outputChannels.length} channels.
+ * @param trimStartFrames The number of audio frames to trim from the start of data written to the
+ * sink after this call.
+ * @param trimEndFrames The number of audio frames to trim from data written to the sink
* immediately preceding the next call to {@link #reset()} or this method.
* @throws ConfigurationException If an error occurs configuring the sink.
*/
- void configure(@C.Encoding int inputEncoding, int inputChannelCount, int inputSampleRate,
- int specifiedBufferSize, @Nullable int[] outputChannels, int trimStartSamples,
- int trimEndSamples) throws ConfigurationException;
+ void configure(
+ @C.Encoding int inputEncoding,
+ int inputChannelCount,
+ int inputSampleRate,
+ int specifiedBufferSize,
+ @Nullable int[] outputChannels,
+ int trimStartFrames,
+ int trimEndFrames)
+ throws ConfigurationException;
/**
* Starts or resumes consuming audio if initialized.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTimestampPoller.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTimestampPoller.java
new file mode 100644
index 0000000000..47120e7375
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTimestampPoller.java
@@ -0,0 +1,307 @@
+/*
+ * 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.audio;
+
+import android.annotation.TargetApi;
+import android.media.AudioTimestamp;
+import android.media.AudioTrack;
+import android.support.annotation.IntDef;
+import android.support.annotation.Nullable;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.util.Util;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Polls the {@link AudioTrack} timestamp, if the platform supports it, taking care of polling at
+ * the appropriate rate to detect when the timestamp starts to advance.
+ *
+ *
When the audio track isn't paused, call {@link #maybePollTimestamp(long)} regularly to check
+ * for timestamp updates. If it returns {@code true}, call {@link #getTimestampPositionFrames()} and
+ * {@link #getTimestampSystemTimeUs()} to access the updated timestamp, then call {@link
+ * #acceptTimestamp()} or {@link #rejectTimestamp()} to accept or reject it.
+ *
+ *
If {@link #hasTimestamp()} returns {@code true}, call {@link #getTimestampSystemTimeUs()} to
+ * get the system time at which the latest timestamp was sampled and {@link
+ * #getTimestampPositionFrames()} to get its position in frames. If {@link #isTimestampAdvancing()}
+ * returns {@code true}, the caller should assume that the timestamp has been increasing in real
+ * time since it was sampled. Otherwise, it may be stationary.
+ *
+ *
Call {@link #reset()} when pausing or resuming the track.
+ */
+/* package */ final class AudioTimestampPoller {
+
+ /** Timestamp polling states. */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ STATE_INITIALIZING,
+ STATE_TIMESTAMP,
+ STATE_TIMESTAMP_ADVANCING,
+ STATE_NO_TIMESTAMP,
+ STATE_ERROR
+ })
+ private @interface State {}
+ /** State when first initializing. */
+ private static final int STATE_INITIALIZING = 0;
+ /** State when we have a timestamp and we don't know if it's advancing. */
+ private static final int STATE_TIMESTAMP = 1;
+ /** State when we have a timestamp and we know it is advancing. */
+ private static final int STATE_TIMESTAMP_ADVANCING = 2;
+ /** State when the no timestamp is available. */
+ private static final int STATE_NO_TIMESTAMP = 3;
+ /** State when the last timestamp was rejected as invalid. */
+ private static final int STATE_ERROR = 4;
+
+ /** The polling interval for {@link #STATE_INITIALIZING} and {@link #STATE_TIMESTAMP}. */
+ private static final int FAST_POLL_INTERVAL_US = 5_000;
+ /**
+ * The polling interval for {@link #STATE_TIMESTAMP_ADVANCING} and {@link #STATE_NO_TIMESTAMP}.
+ */
+ private static final int SLOW_POLL_INTERVAL_US = 10_000_000;
+ /** The polling interval for {@link #STATE_ERROR}. */
+ private static final int ERROR_POLL_INTERVAL_US = 500_000;
+
+ /**
+ * The minimum duration to remain in {@link #STATE_INITIALIZING} if no timestamps are being
+ * returned before transitioning to {@link #STATE_NO_TIMESTAMP}.
+ */
+ private static final int INITIALIZING_DURATION_US = 500_000;
+
+ private final @Nullable AudioTimestampV19 audioTimestamp;
+
+ private @State int state;
+ private long initializeSystemTimeUs;
+ private long sampleIntervalUs;
+ private long lastTimestampSampleTimeUs;
+ private long initialTimestampPositionFrames;
+
+ /**
+ * Creates a new audio timestamp poller.
+ *
+ * @param audioTrack The audio track that will provide timestamps, if the platform supports it.
+ */
+ public AudioTimestampPoller(AudioTrack audioTrack) {
+ if (Util.SDK_INT >= 19) {
+ audioTimestamp = new AudioTimestampV19(audioTrack);
+ reset();
+ } else {
+ audioTimestamp = null;
+ updateState(STATE_NO_TIMESTAMP);
+ }
+ }
+
+ /**
+ * Polls the timestamp if required and returns whether it was updated. If {@code true}, the latest
+ * timestamp is available via {@link #getTimestampSystemTimeUs()} and {@link
+ * #getTimestampPositionFrames()}, and the caller should call {@link #acceptTimestamp()} if the
+ * timestamp was valid, or {@link #rejectTimestamp()} otherwise. The values returned by {@link
+ * #hasTimestamp()} and {@link #isTimestampAdvancing()} may be updated.
+ *
+ * @param systemTimeUs The current system time, in microseconds.
+ * @return Whether the timestamp was updated.
+ */
+ public boolean maybePollTimestamp(long systemTimeUs) {
+ if (audioTimestamp == null || (systemTimeUs - lastTimestampSampleTimeUs) < sampleIntervalUs) {
+ return false;
+ }
+ lastTimestampSampleTimeUs = systemTimeUs;
+ boolean updatedTimestamp = audioTimestamp.maybeUpdateTimestamp();
+ switch (state) {
+ case STATE_INITIALIZING:
+ if (updatedTimestamp) {
+ if (audioTimestamp.getTimestampSystemTimeUs() >= initializeSystemTimeUs) {
+ // We have an initial timestamp, but don't know if it's advancing yet.
+ initialTimestampPositionFrames = audioTimestamp.getTimestampPositionFrames();
+ updateState(STATE_TIMESTAMP);
+ } else {
+ // Drop the timestamp, as it was sampled before the last reset.
+ updatedTimestamp = false;
+ }
+ } else if (systemTimeUs - initializeSystemTimeUs > INITIALIZING_DURATION_US) {
+ // We haven't received a timestamp for a while, so they probably aren't available for the
+ // current audio route. Poll infrequently in case the route changes later.
+ // TODO: Ideally we should listen for audio route changes in order to detect when a
+ // timestamp becomes available again.
+ updateState(STATE_NO_TIMESTAMP);
+ }
+ break;
+ case STATE_TIMESTAMP:
+ if (updatedTimestamp) {
+ long timestampPositionFrames = audioTimestamp.getTimestampPositionFrames();
+ if (timestampPositionFrames > initialTimestampPositionFrames) {
+ updateState(STATE_TIMESTAMP_ADVANCING);
+ }
+ } else {
+ reset();
+ }
+ break;
+ case STATE_TIMESTAMP_ADVANCING:
+ if (!updatedTimestamp) {
+ // The audio route may have changed, so reset polling.
+ reset();
+ }
+ break;
+ case STATE_NO_TIMESTAMP:
+ if (updatedTimestamp) {
+ // The audio route may have changed, so reset polling.
+ reset();
+ }
+ break;
+ case STATE_ERROR:
+ // Do nothing. If the caller accepts any new timestamp we'll reset polling.
+ break;
+ default:
+ throw new IllegalStateException();
+ }
+ return updatedTimestamp;
+ }
+
+ /**
+ * Rejects the timestamp last polled in {@link #maybePollTimestamp(long)}. The instance will enter
+ * the error state and poll timestamps infrequently until the next call to {@link
+ * #acceptTimestamp()}.
+ */
+ public void rejectTimestamp() {
+ updateState(STATE_ERROR);
+ }
+
+ /**
+ * Accepts the timestamp last polled in {@link #maybePollTimestamp(long)}. If the instance is in
+ * the error state, it will begin to poll timestamps frequently again.
+ */
+ public void acceptTimestamp() {
+ if (state == STATE_ERROR) {
+ reset();
+ }
+ }
+
+ /**
+ * Returns whether this instance has a timestamp that can be used to calculate the audio track
+ * position. If {@code true}, call {@link #getTimestampSystemTimeUs()} and {@link
+ * #getTimestampSystemTimeUs()} to access the timestamp.
+ */
+ public boolean hasTimestamp() {
+ return state == STATE_TIMESTAMP || state == STATE_TIMESTAMP_ADVANCING;
+ }
+
+ /**
+ * Returns whether the timestamp appears to be advancing. If {@code true}, call {@link
+ * #getTimestampSystemTimeUs()} and {@link #getTimestampSystemTimeUs()} to access the timestamp. A
+ * current position for the track can be extrapolated based on elapsed real time since the system
+ * time at which the timestamp was sampled.
+ */
+ public boolean isTimestampAdvancing() {
+ return state == STATE_TIMESTAMP_ADVANCING;
+ }
+
+ /** Resets polling. Should be called whenever the audio track is paused or resumed. */
+ public void reset() {
+ if (audioTimestamp != null) {
+ updateState(STATE_INITIALIZING);
+ }
+ }
+
+ /**
+ * If {@link #maybePollTimestamp(long)} or {@link #hasTimestamp()} returned {@code true}, returns
+ * the system time at which the latest timestamp was sampled, in microseconds.
+ */
+ public long getTimestampSystemTimeUs() {
+ return audioTimestamp != null ? audioTimestamp.getTimestampSystemTimeUs() : C.TIME_UNSET;
+ }
+
+ /**
+ * If {@link #maybePollTimestamp(long)} or {@link #hasTimestamp()} returned {@code true}, returns
+ * the latest timestamp's position in frames.
+ */
+ public long getTimestampPositionFrames() {
+ return audioTimestamp != null ? audioTimestamp.getTimestampPositionFrames() : C.POSITION_UNSET;
+ }
+
+ private void updateState(@State int state) {
+ this.state = state;
+ switch (state) {
+ case STATE_INITIALIZING:
+ // Force polling a timestamp immediately, and poll quickly.
+ lastTimestampSampleTimeUs = 0;
+ initialTimestampPositionFrames = C.POSITION_UNSET;
+ initializeSystemTimeUs = System.nanoTime() / 1000;
+ sampleIntervalUs = FAST_POLL_INTERVAL_US;
+ break;
+ case STATE_TIMESTAMP:
+ sampleIntervalUs = FAST_POLL_INTERVAL_US;
+ break;
+ case STATE_TIMESTAMP_ADVANCING:
+ case STATE_NO_TIMESTAMP:
+ sampleIntervalUs = SLOW_POLL_INTERVAL_US;
+ break;
+ case STATE_ERROR:
+ sampleIntervalUs = ERROR_POLL_INTERVAL_US;
+ break;
+ default:
+ throw new IllegalStateException();
+ }
+ }
+
+ @TargetApi(19)
+ private static final class AudioTimestampV19 {
+
+ private final AudioTrack audioTrack;
+ private final AudioTimestamp audioTimestamp;
+
+ private long rawTimestampFramePositionWrapCount;
+ private long lastTimestampRawPositionFrames;
+ private long lastTimestampPositionFrames;
+
+ /**
+ * Creates a new {@link AudioTimestamp} wrapper.
+ *
+ * @param audioTrack The audio track that will provide timestamps.
+ */
+ public AudioTimestampV19(AudioTrack audioTrack) {
+ this.audioTrack = audioTrack;
+ audioTimestamp = new AudioTimestamp();
+ }
+
+ /**
+ * Attempts to update the audio track timestamp. Returns {@code true} if the timestamp was
+ * updated, in which case the updated timestamp system time and position can be accessed with
+ * {@link #getTimestampSystemTimeUs()} and {@link #getTimestampPositionFrames()}. Returns {@code
+ * false} if no timestamp is available, in which case those methods should not be called.
+ */
+ public boolean maybeUpdateTimestamp() {
+ boolean updated = audioTrack.getTimestamp(audioTimestamp);
+ if (updated) {
+ long rawPositionFrames = audioTimestamp.framePosition;
+ if (lastTimestampRawPositionFrames > rawPositionFrames) {
+ // The value must have wrapped around.
+ rawTimestampFramePositionWrapCount++;
+ }
+ lastTimestampRawPositionFrames = rawPositionFrames;
+ lastTimestampPositionFrames =
+ rawPositionFrames + (rawTimestampFramePositionWrapCount << 32);
+ }
+ return updated;
+ }
+
+ public long getTimestampSystemTimeUs() {
+ return audioTimestamp.nanoTime / 1000;
+ }
+
+ public long getTimestampPositionFrames() {
+ return lastTimestampPositionFrames;
+ }
+ }
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java
new file mode 100644
index 0000000000..4714db8902
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java
@@ -0,0 +1,535 @@
+/*
+ * 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.audio;
+
+import android.media.AudioTimestamp;
+import android.media.AudioTrack;
+import android.os.SystemClock;
+import android.support.annotation.IntDef;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.reflect.Method;
+
+/**
+ * Wraps an {@link AudioTrack}, exposing a position based on {@link
+ * AudioTrack#getPlaybackHeadPosition()} and {@link AudioTrack#getTimestamp(AudioTimestamp)}.
+ *
+ *
Call {@link #setAudioTrack(AudioTrack, int, int, int)} to set the audio track to wrap. Call
+ * {@link #mayHandleBuffer(long)} if there is input data to write to the track. If it returns false,
+ * the audio track position is stabilizing and no data may be written. Call {@link #start()}
+ * immediately before calling {@link AudioTrack#play()}. Call {@link #pause()} when pausing the
+ * track. Call {@link #handleEndOfStream(long)} when no more data will be written to the track. When
+ * the audio track will no longer be used, call {@link #reset()}.
+ */
+/* package */ final class AudioTrackPositionTracker {
+
+ /** Listener for position tracker events. */
+ public interface Listener {
+
+ /**
+ * Called when the frame position is too far from the expected frame position.
+ *
+ * @param audioTimestampPositionFrames The frame position of the last known audio track
+ * timestamp.
+ * @param audioTimestampSystemTimeUs The system time associated with the last known audio track
+ * timestamp, in microseconds.
+ * @param systemTimeUs The current time.
+ * @param playbackPositionUs The current playback head position in microseconds.
+ */
+ void onPositionFramesMismatch(
+ long audioTimestampPositionFrames,
+ long audioTimestampSystemTimeUs,
+ long systemTimeUs,
+ long playbackPositionUs);
+
+ /**
+ * Called when the system time associated with the last known audio track timestamp is
+ * unexpectedly far from the current time.
+ *
+ * @param audioTimestampPositionFrames The frame position of the last known audio track
+ * timestamp.
+ * @param audioTimestampSystemTimeUs The system time associated with the last known audio track
+ * timestamp, in microseconds.
+ * @param systemTimeUs The current time.
+ * @param playbackPositionUs The current playback head position in microseconds.
+ */
+ void onSystemTimeUsMismatch(
+ long audioTimestampPositionFrames,
+ long audioTimestampSystemTimeUs,
+ long systemTimeUs,
+ long playbackPositionUs);
+
+ /**
+ * Called when the audio track has provided an invalid latency.
+ *
+ * @param latencyUs The reported latency in microseconds.
+ */
+ void onInvalidLatency(long latencyUs);
+
+ /**
+ * Called when the audio track runs out of data to play.
+ *
+ * @param bufferSize The size of the sink's buffer, in bytes.
+ * @param bufferSizeMs The size of the sink's buffer, in milliseconds, if it is configured for
+ * PCM output. {@link C#TIME_UNSET} if it is configured for encoded audio output, as the
+ * buffered media can have a variable bitrate so the duration may be unknown.
+ */
+ void onUnderrun(int bufferSize, long bufferSizeMs);
+ }
+
+ /** {@link AudioTrack} playback states. */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({PLAYSTATE_STOPPED, PLAYSTATE_PAUSED, PLAYSTATE_PLAYING})
+ private @interface PlayState {}
+ /** @see AudioTrack#PLAYSTATE_STOPPED */
+ private static final int PLAYSTATE_STOPPED = AudioTrack.PLAYSTATE_STOPPED;
+ /** @see AudioTrack#PLAYSTATE_PAUSED */
+ private static final int PLAYSTATE_PAUSED = AudioTrack.PLAYSTATE_PAUSED;
+ /** @see AudioTrack#PLAYSTATE_PLAYING */
+ private static final int PLAYSTATE_PLAYING = AudioTrack.PLAYSTATE_PLAYING;
+
+ /**
+ * AudioTrack timestamps are deemed spurious if they are offset from the system clock by more than
+ * this amount.
+ *
+ *
This is a fail safe that should not be required on correctly functioning devices.
+ */
+ private static final long MAX_AUDIO_TIMESTAMP_OFFSET_US = 5 * C.MICROS_PER_SECOND;
+
+ /**
+ * AudioTrack latencies are deemed impossibly large if they are greater than this amount.
+ *
+ *
This is a fail safe that should not be required on correctly functioning devices.
+ */
+ private static final long MAX_LATENCY_US = 5 * C.MICROS_PER_SECOND;
+
+ private static final long FORCE_RESET_WORKAROUND_TIMEOUT_MS = 200;
+
+ private static final int MAX_PLAYHEAD_OFFSET_COUNT = 10;
+ private static final int MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US = 30000;
+ private static final int MIN_LATENCY_SAMPLE_INTERVAL_US = 500000;
+
+ private final Listener listener;
+ private final long[] playheadOffsets;
+
+ private AudioTrack audioTrack;
+ private int outputPcmFrameSize;
+ private int bufferSize;
+ private AudioTimestampPoller audioTimestampPoller;
+ private int outputSampleRate;
+ private boolean needsPassthroughWorkarounds;
+ private long bufferSizeUs;
+
+ private long smoothedPlayheadOffsetUs;
+ private long lastPlayheadSampleTimeUs;
+
+ private Method getLatencyMethod;
+ private long latencyUs;
+ private boolean hasData;
+
+ private boolean isOutputPcm;
+ private long lastLatencySampleTimeUs;
+ private long lastRawPlaybackHeadPosition;
+ private long rawPlaybackHeadWrapCount;
+ private long passthroughWorkaroundPauseOffset;
+ private int nextPlayheadOffsetIndex;
+ private int playheadOffsetCount;
+ private long stopTimestampUs;
+ private long forceResetWorkaroundTimeMs;
+ private long stopPlaybackHeadPosition;
+ private long endPlaybackHeadPosition;
+
+ /**
+ * Creates a new audio track position tracker.
+ *
+ * @param listener A listener for position tracking events.
+ */
+ public AudioTrackPositionTracker(Listener listener) {
+ this.listener = Assertions.checkNotNull(listener);
+ if (Util.SDK_INT >= 18) {
+ try {
+ getLatencyMethod = AudioTrack.class.getMethod("getLatency", (Class>[]) null);
+ } catch (NoSuchMethodException e) {
+ // There's no guarantee this method exists. Do nothing.
+ }
+ }
+ playheadOffsets = new long[MAX_PLAYHEAD_OFFSET_COUNT];
+ }
+
+ /**
+ * Sets the {@link AudioTrack} to wrap. Subsequent method calls on this instance relate to this
+ * track's position, until the next call to {@link #reset()}.
+ *
+ * @param audioTrack The audio track to wrap.
+ * @param outputEncoding The encoding of the audio track.
+ * @param outputPcmFrameSize For PCM output encodings, the frame size. The value is ignored
+ * otherwise.
+ * @param bufferSize The audio track buffer size in bytes.
+ */
+ public void setAudioTrack(
+ AudioTrack audioTrack,
+ @C.Encoding int outputEncoding,
+ int outputPcmFrameSize,
+ int bufferSize) {
+ this.audioTrack = audioTrack;
+ this.outputPcmFrameSize = outputPcmFrameSize;
+ this.bufferSize = bufferSize;
+ audioTimestampPoller = new AudioTimestampPoller(audioTrack);
+ outputSampleRate = audioTrack.getSampleRate();
+ needsPassthroughWorkarounds = needsPassthroughWorkarounds(outputEncoding);
+ isOutputPcm = Util.isEncodingPcm(outputEncoding);
+ bufferSizeUs = isOutputPcm ? framesToDurationUs(bufferSize / outputPcmFrameSize) : C.TIME_UNSET;
+ lastRawPlaybackHeadPosition = 0;
+ rawPlaybackHeadWrapCount = 0;
+ passthroughWorkaroundPauseOffset = 0;
+ hasData = false;
+ stopTimestampUs = C.TIME_UNSET;
+ forceResetWorkaroundTimeMs = C.TIME_UNSET;
+ latencyUs = 0;
+ }
+
+ public long getCurrentPositionUs(boolean sourceEnded) {
+ if (audioTrack.getPlayState() == PLAYSTATE_PLAYING) {
+ maybeSampleSyncParams();
+ }
+
+ // If the device supports it, use the playback timestamp from AudioTrack.getTimestamp.
+ // Otherwise, derive a smoothed position by sampling the track's frame position.
+ long systemTimeUs = System.nanoTime() / 1000;
+ if (audioTimestampPoller.hasTimestamp()) {
+ // Calculate the speed-adjusted position using the timestamp (which may be in the future).
+ long timestampPositionFrames = audioTimestampPoller.getTimestampPositionFrames();
+ long timestampPositionUs = framesToDurationUs(timestampPositionFrames);
+ if (!audioTimestampPoller.isTimestampAdvancing()) {
+ return timestampPositionUs;
+ }
+ long elapsedSinceTimestampUs = systemTimeUs - audioTimestampPoller.getTimestampSystemTimeUs();
+ return timestampPositionUs + elapsedSinceTimestampUs;
+ } else {
+ long positionUs;
+ if (playheadOffsetCount == 0) {
+ // The AudioTrack has started, but we don't have any samples to compute a smoothed position.
+ positionUs = getPlaybackHeadPositionUs();
+ } else {
+ // getPlaybackHeadPositionUs() only has a granularity of ~20 ms, so we base the position off
+ // the system clock (and a smoothed offset between it and the playhead position) so as to
+ // prevent jitter in the reported positions.
+ positionUs = systemTimeUs + smoothedPlayheadOffsetUs;
+ }
+ if (!sourceEnded) {
+ positionUs -= latencyUs;
+ }
+ return positionUs;
+ }
+ }
+
+ /** Starts position tracking. Must be called immediately before {@link AudioTrack#play()}. */
+ public void start() {
+ audioTimestampPoller.reset();
+ }
+
+ /** Returns whether the audio track is in the playing state. */
+ public boolean isPlaying() {
+ return audioTrack.getPlayState() == PLAYSTATE_PLAYING;
+ }
+
+ /**
+ * Checks the state of the audio track and returns whether the caller can write data to the track.
+ * Notifies {@link Listener#onUnderrun(int, long)} if the track has underrun.
+ *
+ * @param writtenFrames The number of frames that have been written.
+ * @return Whether the caller can write data to the track.
+ */
+ public boolean mayHandleBuffer(long writtenFrames) {
+ @PlayState int playState = audioTrack.getPlayState();
+ if (needsPassthroughWorkarounds) {
+ // An AC-3 audio track continues to play data written while it is paused. Stop writing so its
+ // buffer empties. See [Internal: b/18899620].
+ if (playState == PLAYSTATE_PAUSED) {
+ // We force an underrun to pause the track, so don't notify the listener in this case.
+ hasData = false;
+ return false;
+ }
+
+ // A new AC-3 audio track's playback position continues to increase from the old track's
+ // position for a short time after is has been released. Avoid writing data until the playback
+ // head position actually returns to zero.
+ if (playState == PLAYSTATE_STOPPED && getPlaybackHeadPosition() == 0) {
+ return false;
+ }
+ }
+
+ boolean hadData = hasData;
+ hasData = hasPendingData(writtenFrames);
+ if (hadData && !hasData && playState != PLAYSTATE_STOPPED && listener != null) {
+ listener.onUnderrun(bufferSize, C.usToMs(bufferSizeUs));
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns an estimate of the number of additional bytes that can be written to the audio track's
+ * buffer without running out of space.
+ *
+ *
May only be called if the output encoding is one of the PCM encodings.
+ *
+ * @param writtenBytes The number of bytes written to the audio track so far.
+ * @return An estimate of the number of bytes that can be written.
+ */
+ public int getAvailableBufferSize(long writtenBytes) {
+ int bytesPending = (int) (writtenBytes - (getPlaybackHeadPosition() * outputPcmFrameSize));
+ return bufferSize - bytesPending;
+ }
+
+ /** Returns whether the track is in an invalid state and must be recreated. */
+ public boolean isStalled(long writtenFrames) {
+ return forceResetWorkaroundTimeMs != C.TIME_UNSET
+ && writtenFrames > 0
+ && SystemClock.elapsedRealtime() - forceResetWorkaroundTimeMs
+ >= FORCE_RESET_WORKAROUND_TIMEOUT_MS;
+ }
+
+ /**
+ * Records the writing position at which the stream ended, so that the reported position can
+ * continue to increment while remaining data is played out.
+ *
+ * @param writtenFrames The number of frames that have been written.
+ */
+ public void handleEndOfStream(long writtenFrames) {
+ stopPlaybackHeadPosition = getPlaybackHeadPosition();
+ stopTimestampUs = SystemClock.elapsedRealtime() * 1000;
+ endPlaybackHeadPosition = writtenFrames;
+ }
+
+ /**
+ * Returns whether the audio track has any pending data to play out at its current position.
+ *
+ * @param writtenFrames The number of frames written to the audio track.
+ * @return Whether the audio track has any pending data to play out.
+ */
+ public boolean hasPendingData(long writtenFrames) {
+ return writtenFrames > getPlaybackHeadPosition()
+ || forceHasPendingData();
+ }
+
+ /**
+ * Pauses the audio track position tracker, returning whether the audio track needs to be paused
+ * to cause playback to pause. If {@code false} is returned the audio track will pause without
+ * further interaction, as the end of stream has been handled.
+ */
+ public boolean pause() {
+ resetSyncParams();
+ if (stopTimestampUs == C.TIME_UNSET) {
+ // The audio track is going to be paused, so reset the timestamp poller to ensure it doesn't
+ // supply an advancing position.
+ audioTimestampPoller.reset();
+ return true;
+ }
+ // We've handled the end of the stream already, so there's no need to pause the track.
+ return false;
+ }
+
+ /**
+ * Resets the position tracker. Should be called when the audio track previous passed to {@link
+ * #setAudioTrack(AudioTrack, int, int, int)} is no longer in use.
+ */
+ public void reset() {
+ resetSyncParams();
+ audioTrack = null;
+ audioTimestampPoller = null;
+ }
+
+ private void maybeSampleSyncParams() {
+ long playbackPositionUs = getPlaybackHeadPositionUs();
+ if (playbackPositionUs == 0) {
+ // The AudioTrack hasn't output anything yet.
+ return;
+ }
+ long systemTimeUs = System.nanoTime() / 1000;
+ if (systemTimeUs - lastPlayheadSampleTimeUs >= MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US) {
+ // Take a new sample and update the smoothed offset between the system clock and the playhead.
+ playheadOffsets[nextPlayheadOffsetIndex] = playbackPositionUs - systemTimeUs;
+ nextPlayheadOffsetIndex = (nextPlayheadOffsetIndex + 1) % MAX_PLAYHEAD_OFFSET_COUNT;
+ if (playheadOffsetCount < MAX_PLAYHEAD_OFFSET_COUNT) {
+ playheadOffsetCount++;
+ }
+ lastPlayheadSampleTimeUs = systemTimeUs;
+ smoothedPlayheadOffsetUs = 0;
+ for (int i = 0; i < playheadOffsetCount; i++) {
+ smoothedPlayheadOffsetUs += playheadOffsets[i] / playheadOffsetCount;
+ }
+ }
+
+ if (needsPassthroughWorkarounds) {
+ // Don't sample the timestamp and latency if this is an AC-3 passthrough AudioTrack on
+ // platform API versions 21/22, as incorrect values are returned. See [Internal: b/21145353].
+ return;
+ }
+
+ maybePollAndCheckTimestamp(systemTimeUs, playbackPositionUs);
+ maybeUpdateLatency(systemTimeUs);
+ }
+
+ private void maybePollAndCheckTimestamp(long systemTimeUs, long playbackPositionUs) {
+ if (!audioTimestampPoller.maybePollTimestamp(systemTimeUs)) {
+ return;
+ }
+
+ // Perform sanity checks on the timestamp and accept/reject it.
+ long audioTimestampSystemTimeUs = audioTimestampPoller.getTimestampSystemTimeUs();
+ long audioTimestampPositionFrames = audioTimestampPoller.getTimestampPositionFrames();
+ if (Math.abs(audioTimestampSystemTimeUs - systemTimeUs) > MAX_AUDIO_TIMESTAMP_OFFSET_US) {
+ listener.onSystemTimeUsMismatch(
+ audioTimestampPositionFrames,
+ audioTimestampSystemTimeUs,
+ systemTimeUs,
+ playbackPositionUs);
+ audioTimestampPoller.rejectTimestamp();
+ } else if (Math.abs(framesToDurationUs(audioTimestampPositionFrames) - playbackPositionUs)
+ > MAX_AUDIO_TIMESTAMP_OFFSET_US) {
+ listener.onPositionFramesMismatch(
+ audioTimestampPositionFrames,
+ audioTimestampSystemTimeUs,
+ systemTimeUs,
+ playbackPositionUs);
+ audioTimestampPoller.rejectTimestamp();
+ } else {
+ audioTimestampPoller.acceptTimestamp();
+ }
+ }
+
+ private void maybeUpdateLatency(long systemTimeUs) {
+ if (isOutputPcm
+ && getLatencyMethod != null
+ && systemTimeUs - lastLatencySampleTimeUs >= MIN_LATENCY_SAMPLE_INTERVAL_US) {
+ try {
+ // Compute the audio track latency, excluding the latency due to the buffer (leaving
+ // latency due to the mixer and audio hardware driver).
+ latencyUs =
+ (Integer) getLatencyMethod.invoke(audioTrack, (Object[]) null) * 1000L - bufferSizeUs;
+ // Sanity check that the latency is non-negative.
+ latencyUs = Math.max(latencyUs, 0);
+ // Sanity check that the latency isn't too large.
+ if (latencyUs > MAX_LATENCY_US) {
+ listener.onInvalidLatency(latencyUs);
+ latencyUs = 0;
+ }
+ } catch (Exception e) {
+ // The method existed, but doesn't work. Don't try again.
+ getLatencyMethod = null;
+ }
+ lastLatencySampleTimeUs = systemTimeUs;
+ }
+ }
+
+ private long framesToDurationUs(long frameCount) {
+ return (frameCount * C.MICROS_PER_SECOND) / outputSampleRate;
+ }
+
+ private void resetSyncParams() {
+ smoothedPlayheadOffsetUs = 0;
+ playheadOffsetCount = 0;
+ nextPlayheadOffsetIndex = 0;
+ lastPlayheadSampleTimeUs = 0;
+ }
+
+ /**
+ * If passthrough workarounds are enabled, pausing is implemented by forcing the AudioTrack to
+ * underrun. In this case, still behave as if we have pending data, otherwise writing won't
+ * resume.
+ */
+ private boolean forceHasPendingData() {
+ return needsPassthroughWorkarounds
+ && audioTrack.getPlayState() == AudioTrack.PLAYSTATE_PAUSED
+ && getPlaybackHeadPosition() == 0;
+ }
+
+ /**
+ * Returns whether to work around problems with passthrough audio tracks. See [Internal:
+ * b/18899620, b/19187573, b/21145353].
+ */
+ private static boolean needsPassthroughWorkarounds(@C.Encoding int outputEncoding) {
+ return Util.SDK_INT < 23
+ && (outputEncoding == C.ENCODING_AC3 || outputEncoding == C.ENCODING_E_AC3);
+ }
+
+ private long getPlaybackHeadPositionUs() {
+ return framesToDurationUs(getPlaybackHeadPosition());
+ }
+
+ /**
+ * {@link AudioTrack#getPlaybackHeadPosition()} returns a value intended to be interpreted as an
+ * unsigned 32 bit integer, which also wraps around periodically. This method returns the playback
+ * head position as a long that will only wrap around if the value exceeds {@link Long#MAX_VALUE}
+ * (which in practice will never happen).
+ *
+ * @return The playback head position, in frames.
+ */
+ private long getPlaybackHeadPosition() {
+ if (stopTimestampUs != C.TIME_UNSET) {
+ // Simulate the playback head position up to the total number of frames submitted.
+ long elapsedTimeSinceStopUs = (SystemClock.elapsedRealtime() * 1000) - stopTimestampUs;
+ long framesSinceStop = (elapsedTimeSinceStopUs * outputSampleRate) / C.MICROS_PER_SECOND;
+ return Math.min(endPlaybackHeadPosition, stopPlaybackHeadPosition + framesSinceStop);
+ }
+
+ int state = audioTrack.getPlayState();
+ if (state == PLAYSTATE_STOPPED) {
+ // The audio track hasn't been started.
+ return 0;
+ }
+
+ long rawPlaybackHeadPosition = 0xFFFFFFFFL & audioTrack.getPlaybackHeadPosition();
+ if (needsPassthroughWorkarounds) {
+ // Work around an issue with passthrough/direct AudioTracks on platform API versions 21/22
+ // where the playback head position jumps back to zero on paused passthrough/direct audio
+ // tracks. See [Internal: b/19187573].
+ if (state == PLAYSTATE_PAUSED && rawPlaybackHeadPosition == 0) {
+ passthroughWorkaroundPauseOffset = lastRawPlaybackHeadPosition;
+ }
+ rawPlaybackHeadPosition += passthroughWorkaroundPauseOffset;
+ }
+
+ if (Util.SDK_INT <= 28) {
+ if (rawPlaybackHeadPosition == 0
+ && lastRawPlaybackHeadPosition > 0
+ && state == PLAYSTATE_PLAYING) {
+ // If connecting a Bluetooth audio device fails, the AudioTrack may be left in a state
+ // where its Java API is in the playing state, but the native track is stopped. When this
+ // happens the playback head position gets stuck at zero. In this case, return the old
+ // playback head position and force the track to be reset after
+ // {@link #FORCE_RESET_WORKAROUND_TIMEOUT_MS} has elapsed.
+ if (forceResetWorkaroundTimeMs == C.TIME_UNSET) {
+ forceResetWorkaroundTimeMs = SystemClock.elapsedRealtime();
+ }
+ return lastRawPlaybackHeadPosition;
+ } else {
+ forceResetWorkaroundTimeMs = C.TIME_UNSET;
+ }
+ }
+
+ if (lastRawPlaybackHeadPosition > rawPlaybackHeadPosition) {
+ // The value must have wrapped around.
+ rawPlaybackHeadWrapCount++;
+ }
+ lastRawPlaybackHeadPosition = rawPlaybackHeadPosition;
+ return rawPlaybackHeadPosition + (rawPlaybackHeadWrapCount << 32);
+ }
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java
index 50b484b938..e53eb08c83 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java
@@ -15,9 +15,11 @@
*/
package com.google.android.exoplayer2.audio;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.C.Encoding;
import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.util.Assertions;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
@@ -30,17 +32,15 @@ import java.util.Arrays;
private int channelCount;
private int sampleRateHz;
- private int[] pendingOutputChannels;
+ private @Nullable int[] pendingOutputChannels;
private boolean active;
- private int[] outputChannels;
+ private @Nullable int[] outputChannels;
private ByteBuffer buffer;
private ByteBuffer outputBuffer;
private boolean inputEnded;
- /**
- * Creates a new processor that applies a channel mapping.
- */
+ /** Creates a new processor that applies a channel mapping. */
public ChannelMappingAudioProcessor() {
buffer = EMPTY_BUFFER;
outputBuffer = EMPTY_BUFFER;
@@ -52,9 +52,11 @@ import java.util.Arrays;
* Resets the channel mapping. After calling this method, call {@link #configure(int, int, int)}
* to start using the new channel map.
*
+ * @param outputChannels The mapping from input to output channel indices, or {@code null} to
+ * leave the input unchanged.
* @see AudioSink#configure(int, int, int, int, int[], int, int)
*/
- public void setChannelMap(int[] outputChannels) {
+ public void setChannelMap(@Nullable int[] outputChannels) {
pendingOutputChannels = outputChannels;
}
@@ -110,6 +112,7 @@ import java.util.Arrays;
@Override
public void queueInput(ByteBuffer inputBuffer) {
+ Assertions.checkState(outputChannels != null);
int position = inputBuffer.position();
int limit = inputBuffer.limit();
int frameCount = (limit - position) / (2 * channelCount);
@@ -161,6 +164,7 @@ import java.util.Arrays;
channelCount = Format.NO_VALUE;
sampleRateHz = Format.NO_VALUE;
outputChannels = null;
+ pendingOutputChannels = null;
active = false;
}
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 6d12dc66e8..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
@@ -19,7 +19,6 @@ import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.media.AudioFormat;
import android.media.AudioManager;
-import android.media.AudioTimestamp;
import android.media.AudioTrack;
import android.os.ConditionVariable;
import android.os.SystemClock;
@@ -32,11 +31,12 @@ import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
-import java.lang.reflect.Method;
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
@@ -50,18 +50,108 @@ import java.util.ArrayList;
public final class DefaultAudioSink implements AudioSink {
/**
- * Thrown when {@link AudioTrack#getTimestamp} returns a spurious timestamp, if
- * {@link #failOnSpuriousAudioTimestamp} is set.
+ * Thrown when the audio track has provided a spurious timestamp, if {@link
+ * #failOnSpuriousAudioTimestamp} is set.
*/
public static final class InvalidAudioTrackTimestampException extends RuntimeException {
- /** @param message The detail message for this exception. */
- public InvalidAudioTrackTimestampException(String message) {
+ /**
+ * Creates a new invalid timestamp exception with the specified message.
+ *
+ * @param message The detail message for this exception.
+ */
+ private InvalidAudioTrackTimestampException(String message) {
super(message);
}
}
+ /**
+ * 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.
*/
@@ -80,18 +170,6 @@ public final class DefaultAudioSink implements AudioSink {
*/
private static final int BUFFER_MULTIPLICATION_FACTOR = 4;
- /**
- * @see AudioTrack#PLAYSTATE_STOPPED
- */
- private static final int PLAYSTATE_STOPPED = AudioTrack.PLAYSTATE_STOPPED;
- /**
- * @see AudioTrack#PLAYSTATE_PAUSED
- */
- private static final int PLAYSTATE_PAUSED = AudioTrack.PLAYSTATE_PAUSED;
- /**
- * @see AudioTrack#PLAYSTATE_PLAYING
- */
- private static final int PLAYSTATE_PLAYING = AudioTrack.PLAYSTATE_PLAYING;
/**
* @see AudioTrack#ERROR_BAD_VALUE
*/
@@ -116,21 +194,6 @@ public final class DefaultAudioSink implements AudioSink {
private static final String TAG = "AudioTrack";
- /**
- * AudioTrack timestamps are deemed spurious if they are offset from the system clock by more
- * than this amount.
- *
- * This is a fail safe that should not be required on correctly functioning devices.
- */
- private static final long MAX_AUDIO_TIMESTAMP_OFFSET_US = 5 * C.MICROS_PER_SECOND;
-
- /**
- * AudioTrack latencies are deemed impossibly large if they are greater than this amount.
- *
- * This is a fail safe that should not be required on correctly functioning devices.
- */
- private static final long MAX_LATENCY_US = 5 * C.MICROS_PER_SECOND;
-
/**
* Represents states of the {@link #startMediaTimeUs} value.
*/
@@ -141,10 +204,6 @@ public final class DefaultAudioSink implements AudioSink {
private static final int START_IN_SYNC = 1;
private static final int START_NEED_SYNC = 2;
- private static final int MAX_PLAYHEAD_OFFSET_COUNT = 10;
- private static final int MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US = 30000;
- private static final int MIN_TIMESTAMP_SAMPLE_INTERVAL_US = 500000;
-
/**
* Whether to enable a workaround for an issue where an audio effect does not keep its session
* active across releasing/initializing a new audio track, on platform builds where
@@ -164,51 +223,40 @@ 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 SonicAudioProcessor sonicAudioProcessor;
private final AudioProcessor[] toIntPcmAvailableAudioProcessors;
private final AudioProcessor[] toFloatPcmAvailableAudioProcessors;
private final ConditionVariable releasingConditionVariable;
- private final long[] playheadOffsets;
- private final AudioTrackUtil audioTrackUtil;
+ private final AudioTrackPositionTracker audioTrackPositionTracker;
private final ArrayDeque playbackParametersCheckpoints;
@Nullable private Listener listener;
- /**
- * Used to keep the audio session active on pre-V21 builds (see {@link #initialize()}).
- */
- private AudioTrack keepSessionIdAudioTrack;
+ /** Used to keep the audio session active on pre-V21 builds (see {@link #initialize()}). */
+ @Nullable private AudioTrack keepSessionIdAudioTrack;
+
private AudioTrack audioTrack;
private boolean isInputPcm;
private boolean shouldConvertHighResIntPcmToFloat;
private int inputSampleRate;
- private int sampleRate;
- private int channelConfig;
+ private int outputSampleRate;
+ private int outputChannelConfig;
private @C.Encoding int outputEncoding;
private AudioAttributes audioAttributes;
private boolean processingEnabled;
private boolean canApplyPlaybackParameters;
private int bufferSize;
- private long bufferSizeUs;
- private PlaybackParameters drainingPlaybackParameters;
+ @Nullable private PlaybackParameters afterDrainPlaybackParameters;
private PlaybackParameters playbackParameters;
private long playbackParametersOffsetUs;
private long playbackParametersPositionUs;
- private ByteBuffer avSyncHeader;
+ @Nullable private ByteBuffer avSyncHeader;
private int bytesUntilNextAvSync;
- private int nextPlayheadOffsetIndex;
- private int playheadOffsetCount;
- private long smoothedPlayheadOffsetUs;
- private long lastPlayheadSampleTimeUs;
- private boolean audioTimestampSet;
- private long lastTimestampSampleTimeUs;
-
- private Method getLatencyMethod;
private int pcmFrameSize;
private long submittedPcmBytes;
private long submittedEncodedFrames;
@@ -218,14 +266,12 @@ public final class DefaultAudioSink implements AudioSink {
private int framesPerEncodedSample;
private @StartMediaTimeState int startMediaTimeState;
private long startMediaTimeUs;
- private long resumeSystemTimeUs;
- private long latencyUs;
private float volume;
- private AudioProcessor[] audioProcessors;
+ private AudioProcessor[] activeAudioProcessors;
private ByteBuffer[] outputBuffers;
- private ByteBuffer inputBuffer;
- private ByteBuffer outputBuffer;
+ @Nullable private ByteBuffer inputBuffer;
+ @Nullable private ByteBuffer outputBuffer;
private byte[] preV21OutputBuffer;
private int preV21OutputBufferOffset;
private int drainingAudioProcessorIndex;
@@ -234,21 +280,24 @@ public final class DefaultAudioSink implements AudioSink {
private boolean playing;
private int audioSessionId;
private boolean tunneling;
- private boolean hasData;
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
@@ -262,45 +311,59 @@ 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);
- if (Util.SDK_INT >= 18) {
- try {
- getLatencyMethod =
- AudioTrack.class.getMethod("getLatency", (Class>[]) null);
- } catch (NoSuchMethodException e) {
- // There's no guarantee this method exists. Do nothing.
- }
- }
- if (Util.SDK_INT >= 19) {
- audioTrackUtil = new AudioTrackUtilV19();
- } else {
- audioTrackUtil = new AudioTrackUtil();
- }
+ audioTrackPositionTracker = new AudioTrackPositionTracker(new PositionTrackerListener());
channelMappingAudioProcessor = new ChannelMappingAudioProcessor();
trimmingAudioProcessor = new TrimmingAudioProcessor();
- sonicAudioProcessor = new SonicAudioProcessor();
- toIntPcmAvailableAudioProcessors = new AudioProcessor[4 + audioProcessors.length];
- toIntPcmAvailableAudioProcessors[0] = new ResamplingAudioProcessor();
- toIntPcmAvailableAudioProcessors[1] = channelMappingAudioProcessor;
- toIntPcmAvailableAudioProcessors[2] = trimmingAudioProcessor;
- System.arraycopy(
- audioProcessors, 0, toIntPcmAvailableAudioProcessors, 3, audioProcessors.length);
- toIntPcmAvailableAudioProcessors[3 + 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()};
- playheadOffsets = new long[MAX_PLAYHEAD_OFFSET_COUNT];
volume = 1.0f;
startMediaTimeState = START_NOT_SET;
audioAttributes = AudioAttributes.DEFAULT;
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<>();
}
+ // AudioSink implementation.
+
@Override
public void setListener(Listener listener) {
this.listener = listener;
@@ -308,7 +371,7 @@ public final class DefaultAudioSink implements AudioSink {
@Override
public boolean isEncodingSupported(@C.Encoding int encoding) {
- if (isEncodingPcm(encoding)) {
+ if (Util.isEncodingPcm(encoding)) {
// AudioTrack supports 16-bit integer PCM output in all platform API versions, and float
// output from platform API version 21 only. Other integer PCM encodings are resampled by this
// sink to 16-bit PCM.
@@ -320,52 +383,29 @@ public final class DefaultAudioSink implements AudioSink {
@Override
public long getCurrentPositionUs(boolean sourceEnded) {
- if (!hasCurrentPositionUs()) {
+ if (!isInitialized() || startMediaTimeState == START_NOT_SET) {
return CURRENT_POSITION_NOT_SET;
}
-
- if (audioTrack.getPlayState() == PLAYSTATE_PLAYING) {
- maybeSampleSyncParams();
- }
-
- // If the device supports it, use the playback timestamp from AudioTrack.getTimestamp.
- // Otherwise, derive a smoothed position by sampling the track's frame position.
- long systemClockUs = System.nanoTime() / 1000;
- long positionUs;
- if (audioTimestampSet) {
- // Calculate the speed-adjusted position using the timestamp (which may be in the future).
- long elapsedSinceTimestampUs = systemClockUs - (audioTrackUtil.getTimestampNanoTime() / 1000);
- long elapsedSinceTimestampFrames = durationUsToFrames(elapsedSinceTimestampUs);
- long elapsedFrames = audioTrackUtil.getTimestampFramePosition() + elapsedSinceTimestampFrames;
- positionUs = framesToDurationUs(elapsedFrames);
- } else {
- if (playheadOffsetCount == 0) {
- // The AudioTrack has started, but we don't have any samples to compute a smoothed position.
- positionUs = audioTrackUtil.getPositionUs();
- } else {
- // getPlayheadPositionUs() only has a granularity of ~20 ms, so we base the position off the
- // system clock (and a smoothed offset between it and the playhead position) so as to
- // prevent jitter in the reported positions.
- positionUs = systemClockUs + smoothedPlayheadOffsetUs;
- }
- if (!sourceEnded) {
- positionUs -= latencyUs;
- }
- }
-
+ long positionUs = audioTrackPositionTracker.getCurrentPositionUs(sourceEnded);
positionUs = Math.min(positionUs, framesToDurationUs(getWrittenFrames()));
- return startMediaTimeUs + applySpeedup(positionUs);
+ return startMediaTimeUs + applySkipping(applySpeedup(positionUs));
}
@Override
- public void configure(@C.Encoding int inputEncoding, int inputChannelCount, int inputSampleRate,
- int specifiedBufferSize, @Nullable int[] outputChannels, int trimStartSamples,
- int trimEndSamples) throws ConfigurationException {
+ public void configure(
+ @C.Encoding int inputEncoding,
+ int inputChannelCount,
+ int inputSampleRate,
+ int specifiedBufferSize,
+ @Nullable int[] outputChannels,
+ int trimStartFrames,
+ int trimEndFrames)
+ throws ConfigurationException {
boolean flush = false;
this.inputSampleRate = inputSampleRate;
int channelCount = inputChannelCount;
int sampleRate = inputSampleRate;
- isInputPcm = isEncodingPcm(inputEncoding);
+ isInputPcm = Util.isEncodingPcm(inputEncoding);
shouldConvertHighResIntPcmToFloat =
enableConvertHighResIntPcmToFloat
&& isEncodingSupported(C.ENCODING_PCM_32BIT)
@@ -377,7 +417,7 @@ public final class DefaultAudioSink implements AudioSink {
boolean processingEnabled = isInputPcm && inputEncoding != C.ENCODING_PCM_FLOAT;
canApplyPlaybackParameters = processingEnabled && !shouldConvertHighResIntPcmToFloat;
if (processingEnabled) {
- trimmingAudioProcessor.setTrimSampleCount(trimStartSamples, trimEndSamples);
+ trimmingAudioProcessor.setTrimFrameCount(trimStartFrames, trimEndFrames);
channelMappingAudioProcessor.setChannelMap(outputChannels);
for (AudioProcessor audioProcessor : getAvailableAudioProcessors()) {
try {
@@ -444,8 +484,11 @@ public final class DefaultAudioSink implements AudioSink {
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
}
- if (!flush && isInitialized() && outputEncoding == encoding && this.sampleRate == sampleRate
- && this.channelConfig == channelConfig) {
+ if (!flush
+ && isInitialized()
+ && outputEncoding == encoding
+ && outputSampleRate == sampleRate
+ && outputChannelConfig == channelConfig) {
// We already have an audio track with the correct sample rate, channel config and encoding.
return;
}
@@ -453,12 +496,11 @@ public final class DefaultAudioSink implements AudioSink {
reset();
this.processingEnabled = processingEnabled;
- this.sampleRate = sampleRate;
- this.channelConfig = channelConfig;
+ outputSampleRate = sampleRate;
+ outputChannelConfig = channelConfig;
outputEncoding = encoding;
- if (isInputPcm) {
- outputPcmFrameSize = Util.getPcmFrameSize(outputEncoding, channelCount);
- }
+ outputPcmFrameSize =
+ isInputPcm ? Util.getPcmFrameSize(outputEncoding, channelCount) : C.LENGTH_UNSET;
if (specifiedBufferSize != 0) {
bufferSize = specifiedBufferSize;
} else if (isInputPcm) {
@@ -483,11 +525,9 @@ public final class DefaultAudioSink implements AudioSink {
bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 192 * 6 * 1024 / C.MICROS_PER_SECOND);
}
}
- bufferSizeUs =
- isInputPcm ? framesToDurationUs(bufferSize / outputPcmFrameSize) : C.TIME_UNSET;
}
- private void resetAudioProcessors() {
+ private void setupAudioProcessors() {
ArrayList newAudioProcessors = new ArrayList<>();
for (AudioProcessor audioProcessor : getAvailableAudioProcessors()) {
if (audioProcessor.isActive()) {
@@ -497,10 +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];
- for (int i = 0; i < count; i++) {
- AudioProcessor audioProcessor = audioProcessors[i];
+ flushAudioProcessors();
+ }
+
+ private void flushAudioProcessors() {
+ for (int i = 0; i < activeAudioProcessors.length; i++) {
+ AudioProcessor audioProcessor = activeAudioProcessors[i];
audioProcessor.flush();
outputBuffers[i] = audioProcessor.getOutput();
}
@@ -515,13 +559,6 @@ public final class DefaultAudioSink implements AudioSink {
releasingConditionVariable.block();
audioTrack = initializeAudioTrack();
-
- // The old playback parameters may no longer be applicable so try to reset them now.
- setPlaybackParameters(playbackParameters);
-
- // Flush and reset active audio processors.
- resetAudioProcessors();
-
int audioSessionId = audioTrack.getAudioSessionId();
if (enablePreV21AudioSessionWorkaround) {
if (Util.SDK_INT < 21) {
@@ -543,16 +580,22 @@ public final class DefaultAudioSink implements AudioSink {
}
}
- audioTrackUtil.reconfigure(audioTrack, needsPassthroughWorkarounds());
+ playbackParameters =
+ canApplyPlaybackParameters
+ ? audioProcessorChain.applyPlaybackParameters(playbackParameters)
+ : PlaybackParameters.DEFAULT;
+ setupAudioProcessors();
+
+ audioTrackPositionTracker.setAudioTrack(
+ audioTrack, outputEncoding, outputPcmFrameSize, bufferSize);
setVolumeInternal();
- hasData = false;
}
@Override
public void play() {
playing = true;
if (isInitialized()) {
- resumeSystemTimeUs = System.nanoTime() / 1000;
+ audioTrackPositionTracker.start();
audioTrack.play();
}
}
@@ -577,29 +620,8 @@ public final class DefaultAudioSink implements AudioSink {
}
}
- if (needsPassthroughWorkarounds()) {
- // An AC-3 audio track continues to play data written while it is paused. Stop writing so its
- // buffer empties. See [Internal: b/18899620].
- if (audioTrack.getPlayState() == PLAYSTATE_PAUSED) {
- // We force an underrun to pause the track, so don't notify the listener in this case.
- hasData = false;
- return false;
- }
-
- // A new AC-3 audio track's playback position continues to increase from the old track's
- // position for a short time after is has been released. Avoid writing data until the playback
- // head position actually returns to zero.
- if (audioTrack.getPlayState() == PLAYSTATE_STOPPED
- && audioTrackUtil.getPlaybackHeadPosition() != 0) {
- return false;
- }
- }
-
- boolean hadData = hasData;
- hasData = hasPendingData();
- if (hadData && !hasData && audioTrack.getPlayState() != PLAYSTATE_STOPPED && listener != null) {
- long elapsedSinceLastFeedMs = SystemClock.elapsedRealtime() - lastFeedElapsedRealtimeMs;
- listener.onUnderrun(bufferSize, C.usToMs(bufferSizeUs), elapsedSinceLastFeedMs);
+ if (!audioTrackPositionTracker.mayHandleBuffer(getWrittenFrames())) {
+ return false;
}
if (inputBuffer == null) {
@@ -621,19 +643,22 @@ public final class DefaultAudioSink implements AudioSink {
}
}
- if (drainingPlaybackParameters != null) {
+ if (afterDrainPlaybackParameters != null) {
if (!drainAudioProcessorsToEndOfStream()) {
// Don't process any more input until draining completes.
return false;
}
+ PlaybackParameters newPlaybackParameters = afterDrainPlaybackParameters;
+ afterDrainPlaybackParameters = null;
+ newPlaybackParameters = audioProcessorChain.applyPlaybackParameters(newPlaybackParameters);
// Store the position and corresponding media time from which the parameters will apply.
- playbackParametersCheckpoints.add(new PlaybackParametersCheckpoint(
- drainingPlaybackParameters, Math.max(0, presentationTimeUs),
- framesToDurationUs(getWrittenFrames())));
- drainingPlaybackParameters = null;
- // The audio processors have drained, so flush them. This will cause any active speed
- // adjustment audio processor to start producing audio with the new parameters.
- resetAudioProcessors();
+ playbackParametersCheckpoints.add(
+ new PlaybackParametersCheckpoint(
+ newPlaybackParameters,
+ Math.max(0, presentationTimeUs),
+ framesToDurationUs(getWrittenFrames())));
+ // Update the set of active audio processors to take into account the new parameters.
+ setupAudioProcessors();
}
if (startMediaTimeState == START_NOT_SET) {
@@ -680,7 +705,7 @@ public final class DefaultAudioSink implements AudioSink {
return true;
}
- if (audioTrackUtil.needsReset(getWrittenFrames())) {
+ if (audioTrackPositionTracker.isStalled(getWrittenFrames())) {
Log.w(TAG, "Resetting stalled audio track");
reset();
return true;
@@ -690,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]
@@ -698,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;
@@ -743,9 +768,7 @@ public final class DefaultAudioSink implements AudioSink {
int bytesWritten = 0;
if (Util.SDK_INT < 21) { // isInputPcm == true
// Work out how many bytes we can write without the risk of blocking.
- int bytesPending =
- (int) (writtenPcmBytes - (audioTrackUtil.getPlaybackHeadPosition() * outputPcmFrameSize));
- int bytesToWrite = bufferSize - bytesPending;
+ int bytesToWrite = audioTrackPositionTracker.getAvailableBufferSize(writtenPcmBytes);
if (bytesToWrite > 0) {
bytesToWrite = Math.min(bytesRemaining, bytesToWrite);
bytesWritten = audioTrack.write(preV21OutputBuffer, preV21OutputBufferOffset, bytesToWrite);
@@ -787,7 +810,8 @@ public final class DefaultAudioSink implements AudioSink {
if (drainAudioProcessorsToEndOfStream()) {
// The audio processors have drained, so drain the underlying audio track.
- audioTrackUtil.handleEndOfStream(getWrittenFrames());
+ audioTrackPositionTracker.handleEndOfStream(getWrittenFrames());
+ audioTrack.stop();
bytesUntilNextAvSync = 0;
handledEndOfStream = true;
}
@@ -796,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();
}
@@ -830,9 +854,7 @@ public final class DefaultAudioSink implements AudioSink {
@Override
public boolean hasPendingData() {
- return isInitialized()
- && (getWrittenFrames() > audioTrackUtil.getPlaybackHeadPosition()
- || overrideHasPendingData());
+ return isInitialized() && audioTrackPositionTracker.hasPendingData(getWrittenFrames());
}
@Override
@@ -841,11 +863,9 @@ public final class DefaultAudioSink implements AudioSink {
this.playbackParameters = PlaybackParameters.DEFAULT;
return this.playbackParameters;
}
- playbackParameters = new PlaybackParameters(
- sonicAudioProcessor.setSpeed(playbackParameters.speed),
- sonicAudioProcessor.setPitch(playbackParameters.pitch));
PlaybackParameters lastSetPlaybackParameters =
- drainingPlaybackParameters != null ? drainingPlaybackParameters
+ afterDrainPlaybackParameters != null
+ ? afterDrainPlaybackParameters
: !playbackParametersCheckpoints.isEmpty()
? playbackParametersCheckpoints.getLast().playbackParameters
: this.playbackParameters;
@@ -853,9 +873,10 @@ public final class DefaultAudioSink implements AudioSink {
if (isInitialized()) {
// Drain the audio processors so we can determine the frame position at which the new
// parameters apply.
- drainingPlaybackParameters = playbackParameters;
+ afterDrainPlaybackParameters = playbackParameters;
} else {
- this.playbackParameters = playbackParameters;
+ // Update the playback parameters now.
+ this.playbackParameters = audioProcessorChain.applyPlaybackParameters(playbackParameters);
}
}
return this.playbackParameters;
@@ -928,9 +949,8 @@ public final class DefaultAudioSink implements AudioSink {
@Override
public void pause() {
playing = false;
- if (isInitialized()) {
- resetSyncParams();
- audioTrackUtil.pause();
+ if (isInitialized() && audioTrackPositionTracker.pause()) {
+ audioTrack.pause();
}
}
@@ -942,9 +962,9 @@ public final class DefaultAudioSink implements AudioSink {
writtenPcmBytes = 0;
writtenEncodedFrames = 0;
framesPerEncodedSample = 0;
- if (drainingPlaybackParameters != null) {
- playbackParameters = drainingPlaybackParameters;
- drainingPlaybackParameters = null;
+ if (afterDrainPlaybackParameters != null) {
+ playbackParameters = afterDrainPlaybackParameters;
+ afterDrainPlaybackParameters = null;
} else if (!playbackParametersCheckpoints.isEmpty()) {
playbackParameters = playbackParametersCheckpoints.getLast().playbackParameters;
}
@@ -953,26 +973,19 @@ public final class DefaultAudioSink implements AudioSink {
playbackParametersPositionUs = 0;
inputBuffer = null;
outputBuffer = null;
- for (int i = 0; i < audioProcessors.length; i++) {
- AudioProcessor audioProcessor = audioProcessors[i];
- audioProcessor.flush();
- outputBuffers[i] = audioProcessor.getOutput();
- }
+ flushAudioProcessors();
handledEndOfStream = false;
drainingAudioProcessorIndex = C.INDEX_UNSET;
avSyncHeader = null;
bytesUntilNextAvSync = 0;
startMediaTimeState = START_NOT_SET;
- latencyUs = 0;
- resetSyncParams();
- int playState = audioTrack.getPlayState();
- if (playState == PLAYSTATE_PLAYING) {
+ if (audioTrackPositionTracker.isPlaying()) {
audioTrack.pause();
}
// AudioTrack.release can take some time, so we call it on a background thread.
final AudioTrack toRelease = audioTrack;
audioTrack = null;
- audioTrackUtil.reconfigure(null, false);
+ audioTrackPositionTracker.reset();
releasingConditionVariable.close();
new Thread() {
@Override
@@ -1021,21 +1034,14 @@ public final class DefaultAudioSink implements AudioSink {
}.start();
}
- /**
- * Returns whether {@link #getCurrentPositionUs} can return the current playback position.
- */
- private boolean hasCurrentPositionUs() {
- return isInitialized() && startMediaTimeState != START_NOT_SET;
- }
-
- /**
- * 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;
@@ -1047,96 +1053,17 @@ 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(
positionUs - playbackParametersPositionUs, playbackParameters.speed);
}
- /**
- * Updates the audio track latency and playback position parameters.
- */
- private void maybeSampleSyncParams() {
- long playbackPositionUs = audioTrackUtil.getPositionUs();
- if (playbackPositionUs == 0) {
- // The AudioTrack hasn't output anything yet.
- return;
- }
- long systemClockUs = System.nanoTime() / 1000;
- if (systemClockUs - lastPlayheadSampleTimeUs >= MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US) {
- // Take a new sample and update the smoothed offset between the system clock and the playhead.
- playheadOffsets[nextPlayheadOffsetIndex] = playbackPositionUs - systemClockUs;
- nextPlayheadOffsetIndex = (nextPlayheadOffsetIndex + 1) % MAX_PLAYHEAD_OFFSET_COUNT;
- if (playheadOffsetCount < MAX_PLAYHEAD_OFFSET_COUNT) {
- playheadOffsetCount++;
- }
- lastPlayheadSampleTimeUs = systemClockUs;
- smoothedPlayheadOffsetUs = 0;
- for (int i = 0; i < playheadOffsetCount; i++) {
- smoothedPlayheadOffsetUs += playheadOffsets[i] / playheadOffsetCount;
- }
- }
-
- if (needsPassthroughWorkarounds()) {
- // Don't sample the timestamp and latency if this is an AC-3 passthrough AudioTrack on
- // platform API versions 21/22, as incorrect values are returned. See [Internal: b/21145353].
- return;
- }
-
- if (systemClockUs - lastTimestampSampleTimeUs >= MIN_TIMESTAMP_SAMPLE_INTERVAL_US) {
- audioTimestampSet = audioTrackUtil.updateTimestamp();
- if (audioTimestampSet) {
- // Perform sanity checks on the timestamp.
- long audioTimestampUs = audioTrackUtil.getTimestampNanoTime() / 1000;
- long audioTimestampFramePosition = audioTrackUtil.getTimestampFramePosition();
- if (audioTimestampUs < resumeSystemTimeUs) {
- // The timestamp corresponds to a time before the track was most recently resumed.
- audioTimestampSet = false;
- } else if (Math.abs(audioTimestampUs - systemClockUs) > MAX_AUDIO_TIMESTAMP_OFFSET_US) {
- // The timestamp time base is probably wrong.
- String message = "Spurious audio timestamp (system clock mismatch): "
- + audioTimestampFramePosition + ", " + audioTimestampUs + ", " + systemClockUs + ", "
- + playbackPositionUs + ", " + getSubmittedFrames() + ", " + getWrittenFrames();
- if (failOnSpuriousAudioTimestamp) {
- throw new InvalidAudioTrackTimestampException(message);
- }
- Log.w(TAG, message);
- audioTimestampSet = false;
- } else if (Math.abs(framesToDurationUs(audioTimestampFramePosition) - playbackPositionUs)
- > MAX_AUDIO_TIMESTAMP_OFFSET_US) {
- // The timestamp frame position is probably wrong.
- String message = "Spurious audio timestamp (frame position mismatch): "
- + audioTimestampFramePosition + ", " + audioTimestampUs + ", " + systemClockUs + ", "
- + playbackPositionUs + ", " + getSubmittedFrames() + ", " + getWrittenFrames();
- if (failOnSpuriousAudioTimestamp) {
- throw new InvalidAudioTrackTimestampException(message);
- }
- Log.w(TAG, message);
- audioTimestampSet = false;
- }
- }
- if (getLatencyMethod != null && isInputPcm) {
- try {
- // Compute the audio track latency, excluding the latency due to the buffer (leaving
- // latency due to the mixer and audio hardware driver).
- latencyUs = (Integer) getLatencyMethod.invoke(audioTrack, (Object[]) null) * 1000L
- - bufferSizeUs;
- // Sanity check that the latency is non-negative.
- latencyUs = Math.max(latencyUs, 0);
- // Sanity check that the latency isn't too large.
- if (latencyUs > MAX_LATENCY_US) {
- Log.w(TAG, "Ignoring impossibly large audio latency: " + latencyUs);
- latencyUs = 0;
- }
- } catch (Exception e) {
- // The method existed, but doesn't work. Don't try again.
- getLatencyMethod = null;
- }
- }
- lastTimestampSampleTimeUs = systemClockUs;
- }
+ private long applySkipping(long positionUs) {
+ return positionUs + framesToDurationUs(audioProcessorChain.getSkippedOutputFrameCount());
}
private boolean isInitialized() {
@@ -1148,11 +1075,11 @@ public final class DefaultAudioSink implements AudioSink {
}
private long framesToDurationUs(long frameCount) {
- return (frameCount * C.MICROS_PER_SECOND) / sampleRate;
+ return (frameCount * C.MICROS_PER_SECOND) / outputSampleRate;
}
private long durationUsToFrames(long durationUs) {
- return (durationUs * sampleRate) / C.MICROS_PER_SECOND;
+ return (durationUs * outputSampleRate) / C.MICROS_PER_SECOND;
}
private long getSubmittedFrames() {
@@ -1163,36 +1090,6 @@ public final class DefaultAudioSink implements AudioSink {
return isInputPcm ? (writtenPcmBytes / outputPcmFrameSize) : writtenEncodedFrames;
}
- private void resetSyncParams() {
- smoothedPlayheadOffsetUs = 0;
- playheadOffsetCount = 0;
- nextPlayheadOffsetIndex = 0;
- lastPlayheadSampleTimeUs = 0;
- audioTimestampSet = false;
- lastTimestampSampleTimeUs = 0;
- }
-
- /**
- * Returns whether to work around problems with passthrough audio tracks.
- * See [Internal: b/18899620, b/19187573, b/21145353].
- */
- private boolean needsPassthroughWorkarounds() {
- return Util.SDK_INT < 23
- && (outputEncoding == C.ENCODING_AC3 || outputEncoding == C.ENCODING_E_AC3);
- }
-
- /**
- * Returns whether the audio track should behave as though it has pending data. This is to work
- * around an issue on platform API versions 21/22 where AC-3 audio tracks can't be paused, so we
- * empty their buffers when paused. In this case, they should still behave as if they have
- * pending data, otherwise writing will never resume.
- */
- private boolean overrideHasPendingData() {
- return needsPassthroughWorkarounds()
- && audioTrack.getPlayState() == PLAYSTATE_PAUSED
- && audioTrack.getPlaybackHeadPosition() == 0;
- }
-
private AudioTrack initializeAudioTrack() throws InitializationException {
AudioTrack audioTrack;
if (Util.SDK_INT >= 21) {
@@ -1200,12 +1097,25 @@ public final class DefaultAudioSink implements AudioSink {
} else {
int streamType = Util.getStreamTypeForAudioUsage(audioAttributes.usage);
if (audioSessionId == C.AUDIO_SESSION_ID_UNSET) {
- audioTrack = new AudioTrack(streamType, sampleRate, channelConfig, outputEncoding,
- bufferSize, MODE_STREAM);
+ audioTrack =
+ new AudioTrack(
+ streamType,
+ outputSampleRate,
+ outputChannelConfig,
+ outputEncoding,
+ bufferSize,
+ MODE_STREAM);
} else {
// Re-attach to the same audio session.
- audioTrack = new AudioTrack(streamType, sampleRate, channelConfig, outputEncoding,
- bufferSize, MODE_STREAM, audioSessionId);
+ audioTrack =
+ new AudioTrack(
+ streamType,
+ outputSampleRate,
+ outputChannelConfig,
+ outputEncoding,
+ bufferSize,
+ MODE_STREAM,
+ audioSessionId);
}
}
@@ -1217,7 +1127,7 @@ public final class DefaultAudioSink implements AudioSink {
// The track has already failed to initialize, so it wouldn't be that surprising if release
// were to fail too. Swallow the exception.
}
- throw new InitializationException(state, sampleRate, channelConfig, bufferSize);
+ throw new InitializationException(state, outputSampleRate, outputChannelConfig, bufferSize);
}
return audioTrack;
}
@@ -1234,11 +1144,12 @@ public final class DefaultAudioSink implements AudioSink {
} else {
attributes = audioAttributes.getAudioAttributesV21();
}
- AudioFormat format = new AudioFormat.Builder()
- .setChannelMask(channelConfig)
- .setEncoding(outputEncoding)
- .setSampleRate(sampleRate)
- .build();
+ AudioFormat format =
+ new AudioFormat.Builder()
+ .setChannelMask(outputChannelConfig)
+ .setEncoding(outputEncoding)
+ .setSampleRate(outputSampleRate)
+ .build();
int audioSessionId = this.audioSessionId != C.AUDIO_SESSION_ID_UNSET ? this.audioSessionId
: AudioManager.AUDIO_SESSION_ID_GENERATE;
return new AudioTrack(attributes, format, bufferSize, MODE_STREAM, audioSessionId);
@@ -1259,12 +1170,6 @@ public final class DefaultAudioSink implements AudioSink {
: toIntPcmAvailableAudioProcessors;
}
- private static boolean isEncodingPcm(@C.Encoding int encoding) {
- return encoding == C.ENCODING_PCM_8BIT || encoding == C.ENCODING_PCM_16BIT
- || encoding == C.ENCODING_PCM_24BIT || encoding == C.ENCODING_PCM_32BIT
- || encoding == C.ENCODING_PCM_FLOAT;
- }
-
private static int getFramesPerEncodedSample(@C.Encoding int encoding, ByteBuffer buffer) {
if (encoding == C.ENCODING_DTS || encoding == C.ENCODING_DTS_HD) {
return DtsUtil.parseDtsAudioSampleCount(buffer);
@@ -1273,8 +1178,11 @@ public final class DefaultAudioSink implements AudioSink {
} else if (encoding == C.ENCODING_E_AC3) {
return Ac3Util.parseEAc3SyncframeAudioSampleCount(buffer);
} else if (encoding == C.ENCODING_DOLBY_TRUEHD) {
- return Ac3Util.parseTrueHdSyncframeAudioSampleCount(buffer)
- * Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT;
+ int syncframeOffset = Ac3Util.findTrueHdSyncframeOffset(buffer);
+ return syncframeOffset == C.INDEX_UNSET
+ ? 0
+ : (Ac3Util.parseTrueHdSyncframeAudioSampleCount(buffer, syncframeOffset)
+ * Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT);
} else {
throw new IllegalStateException("Unexpected audio encoding: " + encoding);
}
@@ -1334,237 +1242,6 @@ public final class DefaultAudioSink implements AudioSink {
audioTrack.setStereoVolume(volume, volume);
}
- /**
- * Wraps an {@link AudioTrack} to expose useful utility methods.
- */
- private static class AudioTrackUtil {
-
- private static final long FORCE_RESET_WORKAROUND_TIMEOUT_MS = 200;
-
- protected AudioTrack audioTrack;
- private boolean needsPassthroughWorkaround;
- private int sampleRate;
- private long lastRawPlaybackHeadPosition;
- private long rawPlaybackHeadWrapCount;
- private long passthroughWorkaroundPauseOffset;
-
- private long stopTimestampUs;
- private long forceResetWorkaroundTimeMs;
- private long stopPlaybackHeadPosition;
- private long endPlaybackHeadPosition;
-
- /**
- * Reconfigures the audio track utility helper to use the specified {@code audioTrack}.
- *
- * @param audioTrack The audio track to wrap.
- * @param needsPassthroughWorkaround Whether to workaround issues with pausing AC-3 passthrough
- * audio tracks on platform API version 21/22.
- */
- public void reconfigure(AudioTrack audioTrack, boolean needsPassthroughWorkaround) {
- this.audioTrack = audioTrack;
- this.needsPassthroughWorkaround = needsPassthroughWorkaround;
- stopTimestampUs = C.TIME_UNSET;
- forceResetWorkaroundTimeMs = C.TIME_UNSET;
- lastRawPlaybackHeadPosition = 0;
- rawPlaybackHeadWrapCount = 0;
- passthroughWorkaroundPauseOffset = 0;
- if (audioTrack != null) {
- sampleRate = audioTrack.getSampleRate();
- }
- }
-
- /**
- * Stops the audio track in a way that ensures media written to it is played out in full, and
- * that {@link #getPlaybackHeadPosition()} and {@link #getPositionUs()} continue to increment as
- * the remaining media is played out.
- *
- * @param writtenFrames The total number of frames that have been written.
- */
- public void handleEndOfStream(long writtenFrames) {
- stopPlaybackHeadPosition = getPlaybackHeadPosition();
- stopTimestampUs = SystemClock.elapsedRealtime() * 1000;
- endPlaybackHeadPosition = writtenFrames;
- audioTrack.stop();
- }
-
- /**
- * Pauses the audio track unless the end of the stream has been handled, in which case calling
- * this method does nothing.
- */
- public void pause() {
- if (stopTimestampUs != C.TIME_UNSET) {
- // We don't want to knock the audio track back into the paused state.
- return;
- }
- audioTrack.pause();
- }
-
- /**
- * Returns whether the track is in an invalid state and must be reset.
- *
- * @see #getPlaybackHeadPosition()
- */
- public boolean needsReset(long writtenFrames) {
- return forceResetWorkaroundTimeMs != C.TIME_UNSET && writtenFrames > 0
- && SystemClock.elapsedRealtime() - forceResetWorkaroundTimeMs
- >= FORCE_RESET_WORKAROUND_TIMEOUT_MS;
- }
-
- /**
- * {@link AudioTrack#getPlaybackHeadPosition()} returns a value intended to be interpreted as an
- * unsigned 32 bit integer, which also wraps around periodically. This method returns the
- * playback head position as a long that will only wrap around if the value exceeds
- * {@link Long#MAX_VALUE} (which in practice will never happen).
- *
- * @return The playback head position, in frames.
- */
- public long getPlaybackHeadPosition() {
- if (stopTimestampUs != C.TIME_UNSET) {
- // Simulate the playback head position up to the total number of frames submitted.
- long elapsedTimeSinceStopUs = (SystemClock.elapsedRealtime() * 1000) - stopTimestampUs;
- long framesSinceStop = (elapsedTimeSinceStopUs * sampleRate) / C.MICROS_PER_SECOND;
- return Math.min(endPlaybackHeadPosition, stopPlaybackHeadPosition + framesSinceStop);
- }
-
- int state = audioTrack.getPlayState();
- if (state == PLAYSTATE_STOPPED) {
- // The audio track hasn't been started.
- return 0;
- }
-
- long rawPlaybackHeadPosition = 0xFFFFFFFFL & audioTrack.getPlaybackHeadPosition();
- if (needsPassthroughWorkaround) {
- // Work around an issue with passthrough/direct AudioTracks on platform API versions 21/22
- // where the playback head position jumps back to zero on paused passthrough/direct audio
- // tracks. See [Internal: b/19187573].
- if (state == PLAYSTATE_PAUSED && rawPlaybackHeadPosition == 0) {
- passthroughWorkaroundPauseOffset = lastRawPlaybackHeadPosition;
- }
- rawPlaybackHeadPosition += passthroughWorkaroundPauseOffset;
- }
-
- if (Util.SDK_INT <= 28) {
- if (rawPlaybackHeadPosition == 0 && lastRawPlaybackHeadPosition > 0
- && state == PLAYSTATE_PLAYING) {
- // If connecting a Bluetooth audio device fails, the AudioTrack may be left in a state
- // where its Java API is in the playing state, but the native track is stopped. When this
- // happens the playback head position gets stuck at zero. In this case, return the old
- // playback head position and force the track to be reset after
- // {@link #FORCE_RESET_WORKAROUND_TIMEOUT_MS} has elapsed.
- if (forceResetWorkaroundTimeMs == C.TIME_UNSET) {
- forceResetWorkaroundTimeMs = SystemClock.elapsedRealtime();
- }
- return lastRawPlaybackHeadPosition;
- } else {
- forceResetWorkaroundTimeMs = C.TIME_UNSET;
- }
- }
-
- if (lastRawPlaybackHeadPosition > rawPlaybackHeadPosition) {
- // The value must have wrapped around.
- rawPlaybackHeadWrapCount++;
- }
- lastRawPlaybackHeadPosition = rawPlaybackHeadPosition;
- return rawPlaybackHeadPosition + (rawPlaybackHeadWrapCount << 32);
- }
-
- /**
- * Returns the duration of played media since reconfiguration, in microseconds.
- */
- public long getPositionUs() {
- return (getPlaybackHeadPosition() * C.MICROS_PER_SECOND) / sampleRate;
- }
-
- /**
- * Updates the values returned by {@link #getTimestampNanoTime()} and
- * {@link #getTimestampFramePosition()}.
- *
- * @return Whether the timestamp values were updated.
- */
- public boolean updateTimestamp() {
- return false;
- }
-
- /**
- * Returns the {@link android.media.AudioTimestamp#nanoTime} obtained during the most recent
- * call to {@link #updateTimestamp()} that returned true.
- *
- * @return The nanoTime obtained during the most recent call to {@link #updateTimestamp()} that
- * returned true.
- * @throws UnsupportedOperationException If the implementation does not support audio timestamp
- * queries. {@link #updateTimestamp()} will always return false in this case.
- */
- public long getTimestampNanoTime() {
- // Should never be called if updateTimestamp() returned false.
- throw new UnsupportedOperationException();
- }
-
- /**
- * Returns the {@link android.media.AudioTimestamp#framePosition} obtained during the most
- * recent call to {@link #updateTimestamp()} that returned true. The value is adjusted so that
- * wrap around only occurs if the value exceeds {@link Long#MAX_VALUE} (which in practice will
- * never happen).
- *
- * @return The framePosition obtained during the most recent call to {@link #updateTimestamp()}
- * that returned true.
- * @throws UnsupportedOperationException If the implementation does not support audio timestamp
- * queries. {@link #updateTimestamp()} will always return false in this case.
- */
- public long getTimestampFramePosition() {
- // Should never be called if updateTimestamp() returned false.
- throw new UnsupportedOperationException();
- }
-
- }
-
- @TargetApi(19)
- private static class AudioTrackUtilV19 extends AudioTrackUtil {
-
- private final AudioTimestamp audioTimestamp;
-
- private long rawTimestampFramePositionWrapCount;
- private long lastRawTimestampFramePosition;
- private long lastTimestampFramePosition;
-
- public AudioTrackUtilV19() {
- audioTimestamp = new AudioTimestamp();
- }
-
- @Override
- public void reconfigure(AudioTrack audioTrack, boolean needsPassthroughWorkaround) {
- super.reconfigure(audioTrack, needsPassthroughWorkaround);
- rawTimestampFramePositionWrapCount = 0;
- lastRawTimestampFramePosition = 0;
- lastTimestampFramePosition = 0;
- }
-
- @Override
- public boolean updateTimestamp() {
- boolean updated = audioTrack.getTimestamp(audioTimestamp);
- if (updated) {
- long rawFramePosition = audioTimestamp.framePosition;
- if (lastRawTimestampFramePosition > rawFramePosition) {
- // The value must have wrapped around.
- rawTimestampFramePositionWrapCount++;
- }
- lastRawTimestampFramePosition = rawFramePosition;
- lastTimestampFramePosition = rawFramePosition + (rawTimestampFramePositionWrapCount << 32);
- }
- return updated;
- }
-
- @Override
- public long getTimestampNanoTime() {
- return audioTimestamp.nanoTime;
- }
-
- @Override
- public long getTimestampFramePosition() {
- return lastTimestampFramePosition;
- }
-
- }
-
/**
* Stores playback parameters with the position and media time at which they apply.
*/
@@ -1583,4 +1260,69 @@ public final class DefaultAudioSink implements AudioSink {
}
+ private final class PositionTrackerListener implements AudioTrackPositionTracker.Listener {
+
+ @Override
+ public void onPositionFramesMismatch(
+ long audioTimestampPositionFrames,
+ long audioTimestampSystemTimeUs,
+ long systemTimeUs,
+ long playbackPositionUs) {
+ String message =
+ "Spurious audio timestamp (frame position mismatch): "
+ + audioTimestampPositionFrames
+ + ", "
+ + audioTimestampSystemTimeUs
+ + ", "
+ + systemTimeUs
+ + ", "
+ + playbackPositionUs
+ + ", "
+ + getSubmittedFrames()
+ + ", "
+ + getWrittenFrames();
+ if (failOnSpuriousAudioTimestamp) {
+ throw new InvalidAudioTrackTimestampException(message);
+ }
+ Log.w(TAG, message);
+ }
+
+ @Override
+ public void onSystemTimeUsMismatch(
+ long audioTimestampPositionFrames,
+ long audioTimestampSystemTimeUs,
+ long systemTimeUs,
+ long playbackPositionUs) {
+ String message =
+ "Spurious audio timestamp (system clock mismatch): "
+ + audioTimestampPositionFrames
+ + ", "
+ + audioTimestampSystemTimeUs
+ + ", "
+ + systemTimeUs
+ + ", "
+ + playbackPositionUs
+ + ", "
+ + getSubmittedFrames()
+ + ", "
+ + getWrittenFrames();
+ if (failOnSpuriousAudioTimestamp) {
+ throw new InvalidAudioTrackTimestampException(message);
+ }
+ Log.w(TAG, message);
+ }
+
+ @Override
+ public void onInvalidLatency(long latencyUs) {
+ Log.w(TAG, "Ignoring impossibly large audio latency: " + latencyUs);
+ }
+
+ @Override
+ public void onUnderrun(int bufferSize, long bufferSizeMs) {
+ if (listener != null) {
+ long elapsedSinceLastFeedMs = SystemClock.elapsedRealtime() - lastFeedElapsedRealtimeMs;
+ listener.onUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
+ }
+ }
+ }
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java
index 215b04821b..e3c91cd344 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java
@@ -17,7 +17,6 @@ package com.google.android.exoplayer2.audio;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
-import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
@@ -86,8 +85,6 @@ import java.nio.ByteOrder;
@Override
public void queueInput(ByteBuffer inputBuffer) {
- Assertions.checkState(isActive());
-
boolean isInput32Bit = sourceEncoding == C.ENCODING_PCM_32BIT;
int position = inputBuffer.position();
int limit = inputBuffer.limit();
@@ -150,10 +147,10 @@ import java.nio.ByteOrder;
@Override
public void reset() {
flush();
- buffer = EMPTY_BUFFER;
sampleRateHz = Format.NO_VALUE;
channelCount = Format.NO_VALUE;
sourceEncoding = C.ENCODING_INVALID;
+ buffer = EMPTY_BUFFER;
}
/**
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 33a67554a5..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
@@ -15,7 +15,10 @@
*/
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;
@@ -37,6 +40,7 @@ import com.google.android.exoplayer2.mediacodec.MediaCodecInfo;
import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer;
import com.google.android.exoplayer2.mediacodec.MediaCodecSelector;
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
+import com.google.android.exoplayer2.mediacodec.MediaFormatUtil;
import com.google.android.exoplayer2.util.MediaClock;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
@@ -59,9 +63,11 @@ 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;
+ private int codecMaxInputSize;
private boolean passthroughEnabled;
private boolean codecNeedsDiscardChannelsWorkaround;
private android.media.MediaFormat passthroughMediaFormat;
@@ -75,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.
@@ -91,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.
@@ -121,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.
@@ -146,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.
@@ -169,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());
}
@@ -230,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);
}
@@ -254,20 +309,33 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
@Override
protected void configureCodec(MediaCodecInfo codecInfo, MediaCodec codec, Format format,
MediaCrypto crypto) {
+ codecMaxInputSize = getCodecMaxInputSize(codecInfo, format, getStreamFormats());
codecNeedsDiscardChannelsWorkaround = codecNeedsDiscardChannelsWorkaround(codecInfo.name);
- MediaFormat mediaFormat = getMediaFormatForPlayback(format);
+ 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, 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
public MediaClock getMediaClock() {
return this;
@@ -288,8 +356,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
pcmEncoding = MimeTypes.AUDIO_RAW.equals(newFormat.sampleMimeType) ? newFormat.pcmEncoding
: C.ENCODING_PCM_16BIT;
channelCount = newFormat.channelCount;
- encoderDelay = newFormat.encoderDelay != Format.NO_VALUE ? newFormat.encoderDelay : 0;
- encoderPadding = newFormat.encoderPadding != Format.NO_VALUE ? newFormat.encoderPadding : 0;
+ encoderDelay = newFormat.encoderDelay;
+ encoderPadding = newFormat.encoderPadding;
}
@Override
@@ -380,8 +448,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
@Override
protected void onStopped() {
- audioSink.pause();
updateCurrentPosition();
+ audioSink.pause();
super.onStopped();
}
@@ -494,6 +562,87 @@ 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(
+ 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 format.maxInputSize;
+ }
+
+ /**
+ * Returns the framework {@link MediaFormat} that can be used to configure a {@link MediaCodec}
+ * 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, String codecMimeType, int codecMaxInputSize) {
+ MediaFormat mediaFormat = new MediaFormat();
+ // Set format parameters that should always be set.
+ 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);
+ // Set codec max values.
+ MediaFormatUtil.maybeSetInteger(mediaFormat, MediaFormat.KEY_MAX_INPUT_SIZE, codecMaxInputSize);
+ // Set codec configuration values.
+ if (Util.SDK_INT >= 23) {
+ mediaFormat.setInteger(MediaFormat.KEY_PRIORITY, 0 /* realtime priority */);
+ }
+ return mediaFormat;
+ }
+
private void updateCurrentPosition() {
long newCurrentPositionUs = audioSink.getCurrentPositionUs(isEnded());
if (newCurrentPositionUs != AudioSink.CURRENT_POSITION_NOT_SET) {
@@ -505,6 +654,25 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
}
}
+ /**
+ * Returns whether a codec with suitable maximum input size will support adaptation between two
+ * {@link Format}s.
+ *
+ * @param first The first format.
+ * @param second The second format.
+ * @return Whether the codec will support adaptation between the two {@link Format}s.
+ */
+ private static boolean areAdaptationCompatible(Format first, Format second) {
+ return first.sampleMimeType.equals(second.sampleMimeType)
+ && first.channelCount == second.channelCount
+ && first.sampleRate == second.sampleRate
+ && first.encoderDelay == 0
+ && first.encoderPadding == 0
+ && second.encoderDelay == 0
+ && second.encoderPadding == 0
+ && first.initializationDataEquals(second);
+ }
+
/**
* Returns whether the decoder is known to output six audio channels when provided with input with
* fewer than six channels.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java
index 01123f3c59..eac0bffd65 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java
@@ -28,15 +28,12 @@ import java.nio.ByteOrder;
private int sampleRateHz;
private int channelCount;
- @C.PcmEncoding
- private int encoding;
+ private @C.PcmEncoding int encoding;
private ByteBuffer buffer;
private ByteBuffer outputBuffer;
private boolean inputEnded;
- /**
- * Creates a new audio processor that converts audio data to {@link C#ENCODING_PCM_16BIT}.
- */
+ /** Creates a new audio processor that converts audio data to {@link C#ENCODING_PCM_16BIT}. */
public ResamplingAudioProcessor() {
sampleRateHz = Format.NO_VALUE;
channelCount = Format.NO_VALUE;
@@ -59,9 +56,6 @@ import java.nio.ByteOrder;
this.sampleRateHz = sampleRateHz;
this.channelCount = channelCount;
this.encoding = encoding;
- if (encoding == C.ENCODING_PCM_16BIT) {
- buffer = EMPTY_BUFFER;
- }
return true;
}
@@ -139,6 +133,7 @@ import java.nio.ByteOrder;
}
break;
case C.ENCODING_PCM_16BIT:
+ case C.ENCODING_PCM_FLOAT:
case C.ENCODING_INVALID:
case Format.NO_VALUE:
default:
@@ -177,10 +172,10 @@ import java.nio.ByteOrder;
@Override
public void reset() {
flush();
- buffer = EMPTY_BUFFER;
sampleRateHz = Format.NO_VALUE;
channelCount = Format.NO_VALUE;
encoding = C.ENCODING_INVALID;
+ buffer = EMPTY_BUFFER;
}
}
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
new file mode 100644
index 0000000000..a289ced128
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java
@@ -0,0 +1,412 @@
+/*
+ * 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.audio;
+
+import android.support.annotation.IntDef;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * An {@link AudioProcessor} that skips silence in the input stream. Input and output are 16-bit
+ * PCM.
+ */
+public final class SilenceSkippingAudioProcessor implements AudioProcessor {
+
+ /**
+ * The minimum duration of audio that must be below {@link #SILENCE_THRESHOLD_LEVEL} to classify
+ * that part of audio as silent, in microseconds.
+ */
+ private static final long MINIMUM_SILENCE_DURATION_US = 100_000;
+ /**
+ * The duration of silence by which to extend non-silent sections, in microseconds. The value must
+ * not exceed {@link #MINIMUM_SILENCE_DURATION_US}.
+ */
+ private static final long PADDING_SILENCE_US = 10_000;
+ /**
+ * The absolute level below which an individual PCM sample is classified as silent. Note: the
+ * specified value will be rounded so that the threshold check only depends on the more
+ * significant byte, for efficiency.
+ */
+ private static final short SILENCE_THRESHOLD_LEVEL = 1024;
+
+ /**
+ * Threshold for classifying an individual PCM sample as silent based on its more significant
+ * byte. This is {@link #SILENCE_THRESHOLD_LEVEL} divided by 256 with rounding.
+ */
+ private static final byte SILENCE_THRESHOLD_LEVEL_MSB = (SILENCE_THRESHOLD_LEVEL + 128) >> 8;
+
+ /** Trimming states. */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ STATE_NOISY,
+ STATE_MAYBE_SILENT,
+ STATE_SILENT,
+ })
+ private @interface State {}
+ /** State when the input is not silent. */
+ private static final int STATE_NOISY = 0;
+ /** State when the input may be silent but we haven't read enough yet to know. */
+ private static final int STATE_MAYBE_SILENT = 1;
+ /** State when the input is silent. */
+ private static final int STATE_SILENT = 2;
+
+ private int channelCount;
+ private int sampleRateHz;
+ private int bytesPerFrame;
+
+ private boolean enabled;
+
+ private ByteBuffer buffer;
+ private ByteBuffer outputBuffer;
+ private boolean inputEnded;
+
+ /**
+ * Buffers audio data that may be classified as silence while in {@link #STATE_MAYBE_SILENT}. If
+ * the input becomes noisy before the buffer has filled, it will be output. Otherwise, the buffer
+ * contents will be dropped and the state will transition to {@link #STATE_SILENT}.
+ */
+ private byte[] maybeSilenceBuffer;
+
+ /**
+ * Stores the latest part of the input while silent. It will be output as padding if the next
+ * input is noisy.
+ */
+ private byte[] paddingBuffer;
+
+ private @State int state;
+ private int maybeSilenceBufferSize;
+ private int paddingSize;
+ private boolean hasOutputNoise;
+ private long skippedFrames;
+
+ /** Creates a new silence trimming audio processor. */
+ public SilenceSkippingAudioProcessor() {
+ buffer = EMPTY_BUFFER;
+ outputBuffer = EMPTY_BUFFER;
+ channelCount = Format.NO_VALUE;
+ sampleRateHz = Format.NO_VALUE;
+ maybeSilenceBuffer = new byte[0];
+ paddingBuffer = new byte[0];
+ }
+
+ /**
+ * Sets whether to skip silence in the input. Calling this method will discard any data buffered
+ * within the processor, and may update the value returned by {@link #isActive()}.
+ *
+ * @param enabled Whether to skip silence in the input.
+ */
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ flush();
+ }
+
+ /**
+ * Returns the total number of frames of input audio that were skipped due to being classified as
+ * silence since the last call to {@link #flush()}.
+ */
+ public long getSkippedFrames() {
+ return skippedFrames;
+ }
+
+ // AudioProcessor implementation.
+
+ @Override
+ public boolean configure(int sampleRateHz, int channelCount, int encoding)
+ throws UnhandledFormatException {
+ if (encoding != C.ENCODING_PCM_16BIT) {
+ throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);
+ }
+ if (this.sampleRateHz == sampleRateHz && this.channelCount == channelCount) {
+ return false;
+ }
+ this.sampleRateHz = sampleRateHz;
+ this.channelCount = channelCount;
+ bytesPerFrame = channelCount * 2;
+ return true;
+ }
+
+ @Override
+ public boolean isActive() {
+ return sampleRateHz != Format.NO_VALUE && enabled;
+ }
+
+ @Override
+ public int getOutputChannelCount() {
+ return channelCount;
+ }
+
+ @Override
+ public @C.Encoding int getOutputEncoding() {
+ return C.ENCODING_PCM_16BIT;
+ }
+
+ @Override
+ public int getOutputSampleRateHz() {
+ return sampleRateHz;
+ }
+
+ @Override
+ public void queueInput(ByteBuffer inputBuffer) {
+ while (inputBuffer.hasRemaining() && !outputBuffer.hasRemaining()) {
+ switch (state) {
+ case STATE_NOISY:
+ processNoisy(inputBuffer);
+ break;
+ case STATE_MAYBE_SILENT:
+ processMaybeSilence(inputBuffer);
+ break;
+ case STATE_SILENT:
+ processSilence(inputBuffer);
+ break;
+ default:
+ throw new IllegalStateException();
+ }
+ }
+ }
+
+ @Override
+ public void queueEndOfStream() {
+ inputEnded = true;
+ if (maybeSilenceBufferSize > 0) {
+ // We haven't received enough silence to transition to the silent state, so output the buffer.
+ output(maybeSilenceBuffer, maybeSilenceBufferSize);
+ }
+ if (!hasOutputNoise) {
+ skippedFrames += paddingSize / bytesPerFrame;
+ }
+ }
+
+ @Override
+ public ByteBuffer getOutput() {
+ ByteBuffer outputBuffer = this.outputBuffer;
+ this.outputBuffer = EMPTY_BUFFER;
+ return outputBuffer;
+ }
+
+ @SuppressWarnings("ReferenceEquality")
+ @Override
+ public boolean isEnded() {
+ return inputEnded && outputBuffer == EMPTY_BUFFER;
+ }
+
+ @Override
+ public void flush() {
+ if (isActive()) {
+ int maybeSilenceBufferSize = durationUsToFrames(MINIMUM_SILENCE_DURATION_US) * bytesPerFrame;
+ if (maybeSilenceBuffer.length != maybeSilenceBufferSize) {
+ maybeSilenceBuffer = new byte[maybeSilenceBufferSize];
+ }
+ paddingSize = durationUsToFrames(PADDING_SILENCE_US) * bytesPerFrame;
+ if (paddingBuffer.length != paddingSize) {
+ paddingBuffer = new byte[paddingSize];
+ }
+ }
+ state = STATE_NOISY;
+ outputBuffer = EMPTY_BUFFER;
+ inputEnded = false;
+ skippedFrames = 0;
+ maybeSilenceBufferSize = 0;
+ hasOutputNoise = false;
+ }
+
+ @Override
+ public void reset() {
+ enabled = false;
+ flush();
+ buffer = EMPTY_BUFFER;
+ channelCount = Format.NO_VALUE;
+ sampleRateHz = Format.NO_VALUE;
+ paddingSize = 0;
+ maybeSilenceBuffer = new byte[0];
+ paddingBuffer = new byte[0];
+ }
+
+ // Internal methods.
+
+ /**
+ * Incrementally processes new input from {@code inputBuffer} while in {@link #STATE_NOISY},
+ * updating the state if needed.
+ */
+ private void processNoisy(ByteBuffer inputBuffer) {
+ int limit = inputBuffer.limit();
+
+ // Check if there's any noise within the maybe silence buffer duration.
+ inputBuffer.limit(Math.min(limit, inputBuffer.position() + maybeSilenceBuffer.length));
+ int noiseLimit = findNoiseLimit(inputBuffer);
+ if (noiseLimit == inputBuffer.position()) {
+ // The buffer contains the start of possible silence.
+ state = STATE_MAYBE_SILENT;
+ } else {
+ inputBuffer.limit(noiseLimit);
+ output(inputBuffer);
+ }
+
+ // Restore the limit.
+ inputBuffer.limit(limit);
+ }
+
+ /**
+ * Incrementally processes new input from {@code inputBuffer} while in {@link
+ * #STATE_MAYBE_SILENT}, updating the state if needed.
+ */
+ private void processMaybeSilence(ByteBuffer inputBuffer) {
+ int limit = inputBuffer.limit();
+ int noisePosition = findNoisePosition(inputBuffer);
+ int maybeSilenceInputSize = noisePosition - inputBuffer.position();
+ int maybeSilenceBufferRemaining = maybeSilenceBuffer.length - maybeSilenceBufferSize;
+ if (noisePosition < limit && maybeSilenceInputSize < maybeSilenceBufferRemaining) {
+ // The maybe silence buffer isn't full, so output it and switch back to the noisy state.
+ output(maybeSilenceBuffer, maybeSilenceBufferSize);
+ maybeSilenceBufferSize = 0;
+ state = STATE_NOISY;
+ } else {
+ // Fill as much of the maybe silence buffer as possible.
+ int bytesToWrite = Math.min(maybeSilenceInputSize, maybeSilenceBufferRemaining);
+ inputBuffer.limit(inputBuffer.position() + bytesToWrite);
+ inputBuffer.get(maybeSilenceBuffer, maybeSilenceBufferSize, bytesToWrite);
+ maybeSilenceBufferSize += bytesToWrite;
+ if (maybeSilenceBufferSize == maybeSilenceBuffer.length) {
+ // We've reached a period of silence, so skip it, taking in to account padding for both
+ // the noisy to silent transition and any future silent to noisy transition.
+ if (hasOutputNoise) {
+ output(maybeSilenceBuffer, paddingSize);
+ skippedFrames += (maybeSilenceBufferSize - paddingSize * 2) / bytesPerFrame;
+ } else {
+ skippedFrames += (maybeSilenceBufferSize - paddingSize) / bytesPerFrame;
+ }
+ updatePaddingBuffer(inputBuffer, maybeSilenceBuffer, maybeSilenceBufferSize);
+ maybeSilenceBufferSize = 0;
+ state = STATE_SILENT;
+ }
+
+ // Restore the limit.
+ inputBuffer.limit(limit);
+ }
+ }
+
+ /**
+ * Incrementally processes new input from {@code inputBuffer} while in {@link #STATE_SILENT},
+ * updating the state if needed.
+ */
+ private void processSilence(ByteBuffer inputBuffer) {
+ int limit = inputBuffer.limit();
+ int noisyPosition = findNoisePosition(inputBuffer);
+ inputBuffer.limit(noisyPosition);
+ skippedFrames += inputBuffer.remaining() / bytesPerFrame;
+ updatePaddingBuffer(inputBuffer, paddingBuffer, paddingSize);
+ if (noisyPosition < limit) {
+ // Output the padding, which may include previous input as well as new input, then transition
+ // back to the noisy state.
+ output(paddingBuffer, paddingSize);
+ state = STATE_NOISY;
+
+ // Restore the limit.
+ inputBuffer.limit(limit);
+ }
+ }
+
+ /**
+ * Copies {@code length} elements from {@code data} to populate a new output buffer from the
+ * processor.
+ */
+ private void output(byte[] data, int length) {
+ prepareForOutput(length);
+ buffer.put(data, 0, length);
+ buffer.flip();
+ outputBuffer = buffer;
+ }
+
+ /**
+ * Copies remaining bytes from {@code data} to populate a new output buffer from the processor.
+ */
+ private void output(ByteBuffer data) {
+ prepareForOutput(data.remaining());
+ buffer.put(data);
+ buffer.flip();
+ outputBuffer = buffer;
+ }
+
+ /** Prepares to output {@code size} bytes in {@code buffer}. */
+ private void prepareForOutput(int size) {
+ if (buffer.capacity() < size) {
+ buffer = ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder());
+ } else {
+ buffer.clear();
+ }
+ if (size > 0) {
+ hasOutputNoise = true;
+ }
+ }
+
+ /**
+ * Fills {@link #paddingBuffer} using data from {@code input}, plus any additional buffered data
+ * at the end of {@code buffer} (up to its {@code size}) required to fill it, advancing the input
+ * position.
+ */
+ private void updatePaddingBuffer(ByteBuffer input, byte[] buffer, int size) {
+ int fromInputSize = Math.min(input.remaining(), paddingSize);
+ int fromBufferSize = paddingSize - fromInputSize;
+ System.arraycopy(
+ /* src= */ buffer,
+ /* srcPos= */ size - fromBufferSize,
+ /* dest= */ paddingBuffer,
+ /* destPos= */ 0,
+ /* length= */ fromBufferSize);
+ input.position(input.limit() - fromInputSize);
+ input.get(paddingBuffer, fromBufferSize, fromInputSize);
+ }
+
+ /**
+ * Returns the number of input frames corresponding to {@code durationUs} microseconds of audio.
+ */
+ private int durationUsToFrames(long durationUs) {
+ return (int) ((durationUs * sampleRateHz) / C.MICROS_PER_SECOND);
+ }
+
+ /**
+ * Returns the earliest byte position in [position, limit) of {@code buffer} that contains a frame
+ * classified as a noisy frame, or the limit of the buffer if no such frame exists.
+ */
+ private int findNoisePosition(ByteBuffer buffer) {
+ // The input is in ByteOrder.nativeOrder(), which is little endian on Android.
+ for (int i = buffer.position() + 1; i < buffer.limit(); i += 2) {
+ if (Math.abs(buffer.get(i)) > SILENCE_THRESHOLD_LEVEL_MSB) {
+ // Round to the start of the frame.
+ return bytesPerFrame * (i / bytesPerFrame);
+ }
+ }
+ return buffer.limit();
+ }
+
+ /**
+ * Returns the earliest byte position in [position, limit) of {@code buffer} such that all frames
+ * from the byte position to the limit are classified as silent.
+ */
+ private int findNoiseLimit(ByteBuffer buffer) {
+ // The input is in ByteOrder.nativeOrder(), which is little endian on Android.
+ for (int i = buffer.limit() - 1; i >= buffer.position(); i -= 2) {
+ if (Math.abs(buffer.get(i)) > SILENCE_THRESHOLD_LEVEL_MSB) {
+ // Return the start of the next frame.
+ return bytesPerFrame * (i / bytesPerFrame) + bytesPerFrame;
+ }
+ }
+ return buffer.position();
+ }
+}
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 83c33ee6d7..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);
}
/**
@@ -522,8 +533,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
@Override
protected void onStopped() {
- audioSink.pause();
updateCurrentPosition();
+ audioSink.pause();
}
@Override
@@ -651,8 +662,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
audioTrackNeedsConfigure = true;
}
- encoderDelay = newFormat.encoderDelay == Format.NO_VALUE ? 0 : newFormat.encoderDelay;
- encoderPadding = newFormat.encoderPadding == Format.NO_VALUE ? 0 : newFormat.encoderPadding;
+ encoderDelay = newFormat.encoderDelay;
+ encoderPadding = newFormat.encoderPadding;
eventDispatcher.inputFormatChanged(newFormat);
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java
index daab04e4ab..0bf6baa4d0 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java
@@ -32,27 +32,24 @@ import java.util.Arrays;
private static final int AMDF_FREQUENCY = 4000;
private final int inputSampleRateHz;
- private final int numChannels;
+ private final int channelCount;
private final float speed;
private final float pitch;
private final float rate;
private final int minPeriod;
private final int maxPeriod;
- private final int maxRequired;
+ private final int maxRequiredFrameCount;
private final short[] downSampleBuffer;
- private int inputBufferSize;
private short[] inputBuffer;
- private int outputBufferSize;
+ private int inputFrameCount;
private short[] outputBuffer;
- private int pitchBufferSize;
+ private int outputFrameCount;
private short[] pitchBuffer;
+ private int pitchFrameCount;
private int oldRatePosition;
private int newRatePosition;
- private int numInputSamples;
- private int numOutputSamples;
- private int numPitchSamples;
- private int remainingInputToCopy;
+ private int remainingInputToCopyFrameCount;
private int prevPeriod;
private int prevMinDiff;
private int minDiff;
@@ -62,31 +59,25 @@ import java.util.Arrays;
* Creates a new Sonic audio stream processor.
*
* @param inputSampleRateHz The sample rate of input audio, in hertz.
- * @param numChannels The number of channels in the input audio.
+ * @param channelCount The number of channels in the input audio.
* @param speed The speedup factor for output audio.
* @param pitch The pitch factor for output audio.
* @param outputSampleRateHz The sample rate for output audio, in hertz.
*/
- public Sonic(int inputSampleRateHz, int numChannels, float speed, float pitch,
- int outputSampleRateHz) {
+ public Sonic(
+ int inputSampleRateHz, int channelCount, float speed, float pitch, int outputSampleRateHz) {
this.inputSampleRateHz = inputSampleRateHz;
- this.numChannels = numChannels;
- minPeriod = inputSampleRateHz / MAXIMUM_PITCH;
- maxPeriod = inputSampleRateHz / MINIMUM_PITCH;
- maxRequired = 2 * maxPeriod;
- downSampleBuffer = new short[maxRequired];
- inputBufferSize = maxRequired;
- inputBuffer = new short[maxRequired * numChannels];
- outputBufferSize = maxRequired;
- outputBuffer = new short[maxRequired * numChannels];
- pitchBufferSize = maxRequired;
- pitchBuffer = new short[maxRequired * numChannels];
- oldRatePosition = 0;
- newRatePosition = 0;
- prevPeriod = 0;
+ this.channelCount = channelCount;
this.speed = speed;
this.pitch = pitch;
- this.rate = (float) inputSampleRateHz / outputSampleRateHz;
+ rate = (float) inputSampleRateHz / outputSampleRateHz;
+ minPeriod = inputSampleRateHz / MAXIMUM_PITCH;
+ maxPeriod = inputSampleRateHz / MINIMUM_PITCH;
+ maxRequiredFrameCount = 2 * maxPeriod;
+ downSampleBuffer = new short[maxRequiredFrameCount];
+ inputBuffer = new short[maxRequiredFrameCount * channelCount];
+ outputBuffer = new short[maxRequiredFrameCount * channelCount];
+ pitchBuffer = new short[maxRequiredFrameCount * channelCount];
}
/**
@@ -96,11 +87,11 @@ import java.util.Arrays;
* @param buffer A {@link ShortBuffer} containing input data between its position and limit.
*/
public void queueInput(ShortBuffer buffer) {
- int samplesToWrite = buffer.remaining() / numChannels;
- int bytesToWrite = samplesToWrite * numChannels * 2;
- enlargeInputBufferIfNeeded(samplesToWrite);
- buffer.get(inputBuffer, numInputSamples * numChannels, bytesToWrite / 2);
- numInputSamples += samplesToWrite;
+ int framesToWrite = buffer.remaining() / channelCount;
+ int bytesToWrite = framesToWrite * channelCount * 2;
+ inputBuffer = ensureSpaceForAdditionalFrames(inputBuffer, inputFrameCount, framesToWrite);
+ buffer.get(inputBuffer, inputFrameCount * channelCount, bytesToWrite / 2);
+ inputFrameCount += framesToWrite;
processStreamInput();
}
@@ -111,11 +102,15 @@ import java.util.Arrays;
* @param buffer A {@link ShortBuffer} into which output will be written.
*/
public void getOutput(ShortBuffer buffer) {
- int samplesToRead = Math.min(buffer.remaining() / numChannels, numOutputSamples);
- buffer.put(outputBuffer, 0, samplesToRead * numChannels);
- numOutputSamples -= samplesToRead;
- System.arraycopy(outputBuffer, samplesToRead * numChannels, outputBuffer, 0,
- numOutputSamples * numChannels);
+ int framesToRead = Math.min(buffer.remaining() / channelCount, outputFrameCount);
+ buffer.put(outputBuffer, 0, framesToRead * channelCount);
+ outputFrameCount -= framesToRead;
+ System.arraycopy(
+ outputBuffer,
+ framesToRead * channelCount,
+ outputBuffer,
+ 0,
+ outputFrameCount * channelCount);
}
/**
@@ -123,80 +118,105 @@ import java.util.Arrays;
* added to the output, but flushing in the middle of words could introduce distortion.
*/
public void queueEndOfStream() {
- int remainingSamples = numInputSamples;
+ int remainingFrameCount = inputFrameCount;
float s = speed / pitch;
float r = rate * pitch;
- int expectedOutputSamples =
- numOutputSamples + (int) ((remainingSamples / s + numPitchSamples) / r + 0.5f);
+ int expectedOutputFrames =
+ outputFrameCount + (int) ((remainingFrameCount / s + pitchFrameCount) / r + 0.5f);
// Add enough silence to flush both input and pitch buffers.
- enlargeInputBufferIfNeeded(remainingSamples + 2 * maxRequired);
- for (int xSample = 0; xSample < 2 * maxRequired * numChannels; xSample++) {
- inputBuffer[remainingSamples * numChannels + xSample] = 0;
+ inputBuffer =
+ ensureSpaceForAdditionalFrames(
+ inputBuffer, inputFrameCount, remainingFrameCount + 2 * maxRequiredFrameCount);
+ for (int xSample = 0; xSample < 2 * maxRequiredFrameCount * channelCount; xSample++) {
+ inputBuffer[remainingFrameCount * channelCount + xSample] = 0;
}
- numInputSamples += 2 * maxRequired;
+ inputFrameCount += 2 * maxRequiredFrameCount;
processStreamInput();
- // Throw away any extra samples we generated due to the silence we added.
- if (numOutputSamples > expectedOutputSamples) {
- numOutputSamples = expectedOutputSamples;
+ // Throw away any extra frames we generated due to the silence we added.
+ if (outputFrameCount > expectedOutputFrames) {
+ outputFrameCount = expectedOutputFrames;
}
// Empty input and pitch buffers.
- numInputSamples = 0;
- remainingInputToCopy = 0;
- numPitchSamples = 0;
+ inputFrameCount = 0;
+ remainingInputToCopyFrameCount = 0;
+ pitchFrameCount = 0;
}
- /**
- * Returns the number of output samples that can be read with {@link #getOutput(ShortBuffer)}.
- */
- public int getSamplesAvailable() {
- return numOutputSamples;
+ /** Clears state in preparation for receiving a new stream of input buffers. */
+ public void flush() {
+ inputFrameCount = 0;
+ outputFrameCount = 0;
+ pitchFrameCount = 0;
+ oldRatePosition = 0;
+ newRatePosition = 0;
+ remainingInputToCopyFrameCount = 0;
+ prevPeriod = 0;
+ prevMinDiff = 0;
+ minDiff = 0;
+ maxDiff = 0;
+ }
+
+ /** Returns the number of output frames that can be read with {@link #getOutput(ShortBuffer)}. */
+ public int getFramesAvailable() {
+ return outputFrameCount;
}
// Internal methods.
- private void enlargeOutputBufferIfNeeded(int numSamples) {
- if (numOutputSamples + numSamples > outputBufferSize) {
- outputBufferSize += (outputBufferSize / 2) + numSamples;
- outputBuffer = Arrays.copyOf(outputBuffer, outputBufferSize * numChannels);
+ /**
+ * Returns {@code buffer} or a copy of it, such that there is enough space in the returned buffer
+ * to store {@code newFrameCount} additional frames.
+ *
+ * @param buffer The buffer.
+ * @param frameCount The number of frames already in the buffer.
+ * @param additionalFrameCount The number of additional frames that need to be stored in the
+ * buffer.
+ * @return A buffer with enough space for the additional frames.
+ */
+ private short[] ensureSpaceForAdditionalFrames(
+ short[] buffer, int frameCount, int additionalFrameCount) {
+ int currentCapacityFrames = buffer.length / channelCount;
+ if (frameCount + additionalFrameCount <= currentCapacityFrames) {
+ return buffer;
+ } else {
+ int newCapacityFrames = 3 * currentCapacityFrames / 2 + additionalFrameCount;
+ return Arrays.copyOf(buffer, newCapacityFrames * channelCount);
}
}
- private void enlargeInputBufferIfNeeded(int numSamples) {
- if (numInputSamples + numSamples > inputBufferSize) {
- inputBufferSize += (inputBufferSize / 2) + numSamples;
- inputBuffer = Arrays.copyOf(inputBuffer, inputBufferSize * numChannels);
- }
+ private void removeProcessedInputFrames(int positionFrames) {
+ int remainingFrames = inputFrameCount - positionFrames;
+ System.arraycopy(
+ inputBuffer, positionFrames * channelCount, inputBuffer, 0, remainingFrames * channelCount);
+ inputFrameCount = remainingFrames;
}
- private void removeProcessedInputSamples(int position) {
- int remainingSamples = numInputSamples - position;
- System.arraycopy(inputBuffer, position * numChannels, inputBuffer, 0,
- remainingSamples * numChannels);
- numInputSamples = remainingSamples;
+ private void copyToOutput(short[] samples, int positionFrames, int frameCount) {
+ outputBuffer = ensureSpaceForAdditionalFrames(outputBuffer, outputFrameCount, frameCount);
+ System.arraycopy(
+ samples,
+ positionFrames * channelCount,
+ outputBuffer,
+ outputFrameCount * channelCount,
+ frameCount * channelCount);
+ outputFrameCount += frameCount;
}
- private void copyToOutput(short[] samples, int position, int numSamples) {
- enlargeOutputBufferIfNeeded(numSamples);
- System.arraycopy(samples, position * numChannels, outputBuffer, numOutputSamples * numChannels,
- numSamples * numChannels);
- numOutputSamples += numSamples;
- }
-
- private int copyInputToOutput(int position) {
- int numSamples = Math.min(maxRequired, remainingInputToCopy);
- copyToOutput(inputBuffer, position, numSamples);
- remainingInputToCopy -= numSamples;
- return numSamples;
+ private int copyInputToOutput(int positionFrames) {
+ int frameCount = Math.min(maxRequiredFrameCount, remainingInputToCopyFrameCount);
+ copyToOutput(inputBuffer, positionFrames, frameCount);
+ remainingInputToCopyFrameCount -= frameCount;
+ return frameCount;
}
private void downSampleInput(short[] samples, int position, int skip) {
// If skip is greater than one, average skip samples together and write them to the down-sample
- // buffer. If numChannels is greater than one, mix the channels together as we down sample.
- int numSamples = maxRequired / skip;
- int samplesPerValue = numChannels * skip;
- position *= numChannels;
- for (int i = 0; i < numSamples; i++) {
+ // buffer. If channelCount is greater than one, mix the channels together as we down sample.
+ int frameCount = maxRequiredFrameCount / skip;
+ int samplesPerValue = channelCount * skip;
+ position *= channelCount;
+ for (int i = 0; i < frameCount; i++) {
int value = 0;
for (int j = 0; j < samplesPerValue; j++) {
value += samples[position + i * samplesPerValue + j];
@@ -213,7 +233,7 @@ import java.util.Arrays;
int worstPeriod = 255;
int minDiff = 1;
int maxDiff = 0;
- position *= numChannels;
+ position *= channelCount;
for (int period = minPeriod; period <= maxPeriod; period++) {
int diff = 0;
for (int i = 0; i < period; i++) {
@@ -242,28 +262,22 @@ import java.util.Arrays;
* Returns whether the previous pitch period estimate is a better approximation, which can occur
* at the abrupt end of voiced words.
*/
- private boolean previousPeriodBetter(int minDiff, int maxDiff, boolean preferNewPeriod) {
+ private boolean previousPeriodBetter(int minDiff, int maxDiff) {
if (minDiff == 0 || prevPeriod == 0) {
return false;
}
- if (preferNewPeriod) {
- if (maxDiff > minDiff * 3) {
- // Got a reasonable match this period
- return false;
- }
- if (minDiff * 2 <= prevMinDiff * 3) {
- // Mismatch is not that much greater this period
- return false;
- }
- } else {
- if (minDiff <= prevMinDiff) {
- return false;
- }
+ if (maxDiff > minDiff * 3) {
+ // Got a reasonable match this period.
+ return false;
+ }
+ if (minDiff * 2 <= prevMinDiff * 3) {
+ // Mismatch is not that much greater this period.
+ return false;
}
return true;
}
- private int findPitchPeriod(short[] samples, int position, boolean preferNewPeriod) {
+ private int findPitchPeriod(short[] samples, int position) {
// Find the pitch period. This is a critical step, and we may have to try multiple ways to get a
// good answer. This version uses AMDF. To improve speed, we down sample by an integer factor
// get in the 11 kHz range, and then do it again with a narrower frequency range without down
@@ -271,7 +285,7 @@ import java.util.Arrays;
int period;
int retPeriod;
int skip = inputSampleRateHz > AMDF_FREQUENCY ? inputSampleRateHz / AMDF_FREQUENCY : 1;
- if (numChannels == 1 && skip == 1) {
+ if (channelCount == 1 && skip == 1) {
period = findPitchPeriodInRange(samples, position, minPeriod, maxPeriod);
} else {
downSampleInput(samples, position, skip);
@@ -286,7 +300,7 @@ import java.util.Arrays;
if (maxP > maxPeriod) {
maxP = maxPeriod;
}
- if (numChannels == 1) {
+ if (channelCount == 1) {
period = findPitchPeriodInRange(samples, position, minP, maxP);
} else {
downSampleInput(samples, position, 1);
@@ -294,7 +308,7 @@ import java.util.Arrays;
}
}
}
- if (previousPeriodBetter(minDiff, maxDiff, preferNewPeriod)) {
+ if (previousPeriodBetter(minDiff, maxDiff)) {
retPeriod = prevPeriod;
} else {
retPeriod = period;
@@ -304,30 +318,35 @@ import java.util.Arrays;
return retPeriod;
}
- private void moveNewSamplesToPitchBuffer(int originalNumOutputSamples) {
- int numSamples = numOutputSamples - originalNumOutputSamples;
- if (numPitchSamples + numSamples > pitchBufferSize) {
- pitchBufferSize += (pitchBufferSize / 2) + numSamples;
- pitchBuffer = Arrays.copyOf(pitchBuffer, pitchBufferSize * numChannels);
- }
- System.arraycopy(outputBuffer, originalNumOutputSamples * numChannels, pitchBuffer,
- numPitchSamples * numChannels, numSamples * numChannels);
- numOutputSamples = originalNumOutputSamples;
- numPitchSamples += numSamples;
+ private void moveNewSamplesToPitchBuffer(int originalOutputFrameCount) {
+ int frameCount = outputFrameCount - originalOutputFrameCount;
+ pitchBuffer = ensureSpaceForAdditionalFrames(pitchBuffer, pitchFrameCount, frameCount);
+ System.arraycopy(
+ outputBuffer,
+ originalOutputFrameCount * channelCount,
+ pitchBuffer,
+ pitchFrameCount * channelCount,
+ frameCount * channelCount);
+ outputFrameCount = originalOutputFrameCount;
+ pitchFrameCount += frameCount;
}
- private void removePitchSamples(int numSamples) {
- if (numSamples == 0) {
+ private void removePitchFrames(int frameCount) {
+ if (frameCount == 0) {
return;
}
- System.arraycopy(pitchBuffer, numSamples * numChannels, pitchBuffer, 0,
- (numPitchSamples - numSamples) * numChannels);
- numPitchSamples -= numSamples;
+ System.arraycopy(
+ pitchBuffer,
+ frameCount * channelCount,
+ pitchBuffer,
+ 0,
+ (pitchFrameCount - frameCount) * channelCount);
+ pitchFrameCount -= frameCount;
}
private short interpolate(short[] in, int inPos, int oldSampleRate, int newSampleRate) {
short left = in[inPos];
- short right = in[inPos + numChannels];
+ short right = in[inPos + channelCount];
int position = newRatePosition * oldSampleRate;
int leftPosition = oldRatePosition * newSampleRate;
int rightPosition = (oldRatePosition + 1) * newSampleRate;
@@ -336,8 +355,8 @@ import java.util.Arrays;
return (short) ((ratio * left + (width - ratio) * right) / width);
}
- private void adjustRate(float rate, int originalNumOutputSamples) {
- if (numOutputSamples == originalNumOutputSamples) {
+ private void adjustRate(float rate, int originalOutputFrameCount) {
+ if (outputFrameCount == originalOutputFrameCount) {
return;
}
int newSampleRate = (int) (inputSampleRateHz / rate);
@@ -347,17 +366,19 @@ import java.util.Arrays;
newSampleRate /= 2;
oldSampleRate /= 2;
}
- moveNewSamplesToPitchBuffer(originalNumOutputSamples);
+ moveNewSamplesToPitchBuffer(originalOutputFrameCount);
// Leave at least one pitch sample in the buffer.
- for (int position = 0; position < numPitchSamples - 1; position++) {
+ for (int position = 0; position < pitchFrameCount - 1; position++) {
while ((oldRatePosition + 1) * newSampleRate > newRatePosition * oldSampleRate) {
- enlargeOutputBufferIfNeeded(1);
- for (int i = 0; i < numChannels; i++) {
- outputBuffer[numOutputSamples * numChannels + i] =
- interpolate(pitchBuffer, position * numChannels + i, oldSampleRate, newSampleRate);
+ outputBuffer =
+ ensureSpaceForAdditionalFrames(
+ outputBuffer, outputFrameCount, /* additionalFrameCount= */ 1);
+ for (int i = 0; i < channelCount; i++) {
+ outputBuffer[outputFrameCount * channelCount + i] =
+ interpolate(pitchBuffer, position * channelCount + i, oldSampleRate, newSampleRate);
}
newRatePosition++;
- numOutputSamples++;
+ outputFrameCount++;
}
oldRatePosition++;
if (oldRatePosition == oldSampleRate) {
@@ -366,91 +387,117 @@ import java.util.Arrays;
newRatePosition = 0;
}
}
- removePitchSamples(numPitchSamples - 1);
+ removePitchFrames(pitchFrameCount - 1);
}
private int skipPitchPeriod(short[] samples, int position, float speed, int period) {
// Skip over a pitch period, and copy period/speed samples to the output.
- int newSamples;
+ int newFrameCount;
if (speed >= 2.0f) {
- newSamples = (int) (period / (speed - 1.0f));
+ newFrameCount = (int) (period / (speed - 1.0f));
} else {
- newSamples = period;
- remainingInputToCopy = (int) (period * (2.0f - speed) / (speed - 1.0f));
+ newFrameCount = period;
+ remainingInputToCopyFrameCount = (int) (period * (2.0f - speed) / (speed - 1.0f));
}
- enlargeOutputBufferIfNeeded(newSamples);
- overlapAdd(newSamples, numChannels, outputBuffer, numOutputSamples, samples, position, samples,
+ outputBuffer = ensureSpaceForAdditionalFrames(outputBuffer, outputFrameCount, newFrameCount);
+ overlapAdd(
+ newFrameCount,
+ channelCount,
+ outputBuffer,
+ outputFrameCount,
+ samples,
+ position,
+ samples,
position + period);
- numOutputSamples += newSamples;
- return newSamples;
+ outputFrameCount += newFrameCount;
+ return newFrameCount;
}
private int insertPitchPeriod(short[] samples, int position, float speed, int period) {
// Insert a pitch period, and determine how much input to copy directly.
- int newSamples;
+ int newFrameCount;
if (speed < 0.5f) {
- newSamples = (int) (period * speed / (1.0f - speed));
+ newFrameCount = (int) (period * speed / (1.0f - speed));
} else {
- newSamples = period;
- remainingInputToCopy = (int) (period * (2.0f * speed - 1.0f) / (1.0f - speed));
+ newFrameCount = period;
+ remainingInputToCopyFrameCount = (int) (period * (2.0f * speed - 1.0f) / (1.0f - speed));
}
- enlargeOutputBufferIfNeeded(period + newSamples);
- System.arraycopy(samples, position * numChannels, outputBuffer, numOutputSamples * numChannels,
- period * numChannels);
- overlapAdd(newSamples, numChannels, outputBuffer, numOutputSamples + period, samples,
- position + period, samples, position);
- numOutputSamples += period + newSamples;
- return newSamples;
+ outputBuffer =
+ ensureSpaceForAdditionalFrames(outputBuffer, outputFrameCount, period + newFrameCount);
+ System.arraycopy(
+ samples,
+ position * channelCount,
+ outputBuffer,
+ outputFrameCount * channelCount,
+ period * channelCount);
+ overlapAdd(
+ newFrameCount,
+ channelCount,
+ outputBuffer,
+ outputFrameCount + period,
+ samples,
+ position + period,
+ samples,
+ position);
+ outputFrameCount += period + newFrameCount;
+ return newFrameCount;
}
private void changeSpeed(float speed) {
- if (numInputSamples < maxRequired) {
+ if (inputFrameCount < maxRequiredFrameCount) {
return;
}
- int numSamples = numInputSamples;
- int position = 0;
+ int frameCount = inputFrameCount;
+ int positionFrames = 0;
do {
- if (remainingInputToCopy > 0) {
- position += copyInputToOutput(position);
+ if (remainingInputToCopyFrameCount > 0) {
+ positionFrames += copyInputToOutput(positionFrames);
} else {
- int period = findPitchPeriod(inputBuffer, position, true);
+ int period = findPitchPeriod(inputBuffer, positionFrames);
if (speed > 1.0) {
- position += period + skipPitchPeriod(inputBuffer, position, speed, period);
+ positionFrames += period + skipPitchPeriod(inputBuffer, positionFrames, speed, period);
} else {
- position += insertPitchPeriod(inputBuffer, position, speed, period);
+ positionFrames += insertPitchPeriod(inputBuffer, positionFrames, speed, period);
}
}
- } while (position + maxRequired <= numSamples);
- removeProcessedInputSamples(position);
+ } while (positionFrames + maxRequiredFrameCount <= frameCount);
+ removeProcessedInputFrames(positionFrames);
}
private void processStreamInput() {
// Resample as many pitch periods as we have buffered on the input.
- int originalNumOutputSamples = numOutputSamples;
+ int originalOutputFrameCount = outputFrameCount;
float s = speed / pitch;
float r = rate * pitch;
if (s > 1.00001 || s < 0.99999) {
changeSpeed(s);
} else {
- copyToOutput(inputBuffer, 0, numInputSamples);
- numInputSamples = 0;
+ copyToOutput(inputBuffer, 0, inputFrameCount);
+ inputFrameCount = 0;
}
if (r != 1.0f) {
- adjustRate(r, originalNumOutputSamples);
+ adjustRate(r, originalOutputFrameCount);
}
}
- private static void overlapAdd(int numSamples, int numChannels, short[] out, int outPos,
- short[] rampDown, int rampDownPos, short[] rampUp, int rampUpPos) {
- for (int i = 0; i < numChannels; i++) {
- int o = outPos * numChannels + i;
- int u = rampUpPos * numChannels + i;
- int d = rampDownPos * numChannels + i;
- for (int t = 0; t < numSamples; t++) {
- out[o] = (short) ((rampDown[d] * (numSamples - t) + rampUp[u] * t) / numSamples);
- o += numChannels;
- d += numChannels;
- u += numChannels;
+ private static void overlapAdd(
+ int frameCount,
+ int channelCount,
+ short[] out,
+ int outPosition,
+ short[] rampDown,
+ int rampDownPosition,
+ short[] rampUp,
+ int rampUpPosition) {
+ for (int i = 0; i < channelCount; i++) {
+ int o = outPosition * channelCount + i;
+ int u = rampUpPosition * channelCount + i;
+ int d = rampDownPosition * channelCount + i;
+ for (int t = 0; t < frameCount; t++) {
+ out[o] = (short) ((rampDown[d] * (frameCount - t) + rampUp[u] * t) / frameCount);
+ o += channelCount;
+ d += channelCount;
+ u += channelCount;
}
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SonicAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SonicAudioProcessor.java
index 370ddb2809..2ca2d47828 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/SonicAudioProcessor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SonicAudioProcessor.java
@@ -15,9 +15,11 @@
*/
package com.google.android.exoplayer2.audio;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.C.Encoding;
import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
@@ -60,15 +62,14 @@ public final class SonicAudioProcessor implements AudioProcessor {
*/
private static final int MIN_BYTES_FOR_SPEEDUP_CALCULATION = 1024;
- private int pendingOutputSampleRateHz;
private int channelCount;
private int sampleRateHz;
-
- private Sonic sonic;
private float speed;
private float pitch;
private int outputSampleRateHz;
+ private int pendingOutputSampleRateHz;
+ private @Nullable Sonic sonic;
private ByteBuffer buffer;
private ShortBuffer shortBuffer;
private ByteBuffer outputBuffer;
@@ -92,24 +93,36 @@ public final class SonicAudioProcessor implements AudioProcessor {
}
/**
- * Sets the playback speed. The new speed will take effect after a call to {@link #flush()}.
+ * Sets the playback speed. Calling this method will discard any data buffered within the
+ * processor, and may update the value returned by {@link #isActive()}.
*
* @param speed The requested new playback speed.
* @return The actual new playback speed.
*/
public float setSpeed(float speed) {
- this.speed = Util.constrainValue(speed, MINIMUM_SPEED, MAXIMUM_SPEED);
- return this.speed;
+ speed = Util.constrainValue(speed, MINIMUM_SPEED, MAXIMUM_SPEED);
+ if (this.speed != speed) {
+ this.speed = speed;
+ sonic = null;
+ }
+ flush();
+ return speed;
}
/**
- * Sets the playback pitch. The new pitch will take effect after a call to {@link #flush()}.
+ * Sets the playback pitch. Calling this method will discard any data buffered within the
+ * processor, and may update the value returned by {@link #isActive()}.
*
* @param pitch The requested new pitch.
* @return The actual new pitch.
*/
public float setPitch(float pitch) {
- this.pitch = Util.constrainValue(pitch, MINIMUM_PITCH, MAXIMUM_PITCH);
+ pitch = Util.constrainValue(pitch, MINIMUM_PITCH, MAXIMUM_PITCH);
+ if (this.pitch != pitch) {
+ this.pitch = pitch;
+ sonic = null;
+ }
+ flush();
return pitch;
}
@@ -159,13 +172,16 @@ public final class SonicAudioProcessor implements AudioProcessor {
this.sampleRateHz = sampleRateHz;
this.channelCount = channelCount;
this.outputSampleRateHz = outputSampleRateHz;
+ sonic = null;
return true;
}
@Override
public boolean isActive() {
- return Math.abs(speed - 1f) >= CLOSE_THRESHOLD || Math.abs(pitch - 1f) >= CLOSE_THRESHOLD
- || outputSampleRateHz != sampleRateHz;
+ return sampleRateHz != Format.NO_VALUE
+ && (Math.abs(speed - 1f) >= CLOSE_THRESHOLD
+ || Math.abs(pitch - 1f) >= CLOSE_THRESHOLD
+ || outputSampleRateHz != sampleRateHz);
}
@Override
@@ -185,6 +201,7 @@ public final class SonicAudioProcessor implements AudioProcessor {
@Override
public void queueInput(ByteBuffer inputBuffer) {
+ Assertions.checkState(sonic != null);
if (inputBuffer.hasRemaining()) {
ShortBuffer shortBuffer = inputBuffer.asShortBuffer();
int inputSize = inputBuffer.remaining();
@@ -192,7 +209,7 @@ public final class SonicAudioProcessor implements AudioProcessor {
sonic.queueInput(shortBuffer);
inputBuffer.position(inputBuffer.position() + inputSize);
}
- int outputSize = sonic.getSamplesAvailable() * channelCount * 2;
+ int outputSize = sonic.getFramesAvailable() * channelCount * 2;
if (outputSize > 0) {
if (buffer.capacity() < outputSize) {
buffer = ByteBuffer.allocateDirect(outputSize).order(ByteOrder.nativeOrder());
@@ -210,6 +227,7 @@ public final class SonicAudioProcessor implements AudioProcessor {
@Override
public void queueEndOfStream() {
+ Assertions.checkState(sonic != null);
sonic.queueEndOfStream();
inputEnded = true;
}
@@ -223,12 +241,18 @@ public final class SonicAudioProcessor implements AudioProcessor {
@Override
public boolean isEnded() {
- return inputEnded && (sonic == null || sonic.getSamplesAvailable() == 0);
+ return inputEnded && (sonic == null || sonic.getFramesAvailable() == 0);
}
@Override
public void flush() {
- sonic = new Sonic(sampleRateHz, channelCount, speed, pitch, outputSampleRateHz);
+ if (isActive()) {
+ if (sonic == null) {
+ sonic = new Sonic(sampleRateHz, channelCount, speed, pitch, outputSampleRateHz);
+ } else {
+ sonic.flush();
+ }
+ }
outputBuffer = EMPTY_BUFFER;
inputBytes = 0;
outputBytes = 0;
@@ -237,17 +261,19 @@ public final class SonicAudioProcessor implements AudioProcessor {
@Override
public void reset() {
- sonic = null;
- buffer = EMPTY_BUFFER;
- shortBuffer = buffer.asShortBuffer();
- outputBuffer = EMPTY_BUFFER;
+ speed = 1f;
+ pitch = 1f;
channelCount = Format.NO_VALUE;
sampleRateHz = Format.NO_VALUE;
outputSampleRateHz = Format.NO_VALUE;
+ buffer = EMPTY_BUFFER;
+ shortBuffer = buffer.asShortBuffer();
+ outputBuffer = EMPTY_BUFFER;
+ pendingOutputSampleRateHz = SAMPLE_RATE_NO_CHANGE;
+ sonic = null;
inputBytes = 0;
outputBytes = 0;
inputEnded = false;
- pendingOutputSampleRateHz = SAMPLE_RATE_NO_CHANGE;
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java
index 9ff1c158dd..ccaa9c3fed 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java
@@ -22,14 +22,12 @@ import com.google.android.exoplayer2.util.Util;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
-/**
- * Audio processor for trimming samples from the start/end of data.
- */
+/** Audio processor for trimming samples from the start/end of data. */
/* package */ final class TrimmingAudioProcessor implements AudioProcessor {
private boolean isActive;
- private int trimStartSamples;
- private int trimEndSamples;
+ private int trimStartFrames;
+ private int trimEndFrames;
private int channelCount;
private int sampleRateHz;
@@ -40,27 +38,27 @@ import java.nio.ByteOrder;
private int endBufferSize;
private boolean inputEnded;
- /**
- * Creates a new audio processor for trimming samples from the start/end of data.
- */
+ /** Creates a new audio processor for trimming samples from the start/end of data. */
public TrimmingAudioProcessor() {
buffer = EMPTY_BUFFER;
outputBuffer = EMPTY_BUFFER;
channelCount = Format.NO_VALUE;
+ sampleRateHz = Format.NO_VALUE;
+ endBuffer = new byte[0];
}
/**
- * Sets the number of audio samples to trim from the start and end of audio passed to this
+ * Sets the number of audio frames to trim from the start and end of audio passed to this
* processor. After calling this method, call {@link #configure(int, int, int)} to apply the new
- * trimming sample counts.
+ * trimming frame counts.
*
- * @param trimStartSamples The number of audio samples to trim from the start of audio.
- * @param trimEndSamples The number of audio samples to trim from the end of audio.
+ * @param trimStartFrames The number of audio frames to trim from the start of audio.
+ * @param trimEndFrames The number of audio frames to trim from the end of audio.
* @see AudioSink#configure(int, int, int, int, int[], int, int)
*/
- public void setTrimSampleCount(int trimStartSamples, int trimEndSamples) {
- this.trimStartSamples = trimStartSamples;
- this.trimEndSamples = trimEndSamples;
+ public void setTrimFrameCount(int trimStartFrames, int trimEndFrames) {
+ this.trimStartFrames = trimStartFrames;
+ this.trimEndFrames = trimEndFrames;
}
@Override
@@ -71,11 +69,11 @@ import java.nio.ByteOrder;
}
this.channelCount = channelCount;
this.sampleRateHz = sampleRateHz;
- endBuffer = new byte[trimEndSamples * channelCount * 2];
+ endBuffer = new byte[trimEndFrames * channelCount * 2];
endBufferSize = 0;
- pendingTrimStartBytes = trimStartSamples * channelCount * 2;
+ pendingTrimStartBytes = trimStartFrames * channelCount * 2;
boolean wasActive = isActive;
- isActive = trimStartSamples != 0 || trimEndSamples != 0;
+ isActive = trimStartFrames != 0 || trimEndFrames != 0;
return wasActive != isActive;
}
@@ -182,7 +180,7 @@ import java.nio.ByteOrder;
buffer = EMPTY_BUFFER;
channelCount = Format.NO_VALUE;
sampleRateHz = Format.NO_VALUE;
- endBuffer = null;
+ endBuffer = new byte[0];
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/ClearKeyUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/ClearKeyUtil.java
index ee337dcc51..87dbc7a65c 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/drm/ClearKeyUtil.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/ClearKeyUtil.java
@@ -17,8 +17,6 @@ package com.google.android.exoplayer2.drm;
import android.util.Log;
import com.google.android.exoplayer2.util.Util;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
@@ -29,7 +27,6 @@ import org.json.JSONObject;
/* package */ final class ClearKeyUtil {
private static final String TAG = "ClearKeyUtil";
- private static final Pattern REQUEST_KIDS_PATTERN = Pattern.compile("\"kids\":\\[\"(.*?)\"]");
private ClearKeyUtil() {}
@@ -43,21 +40,12 @@ import org.json.JSONObject;
if (Util.SDK_INT >= 27) {
return request;
}
- // Prior to O-MR1 the ClearKey CDM encoded the values in the "kids" array using Base64 rather
- // than Base64Url. See [Internal: b/64388098]. Any "/" characters that ended up in the request
- // as a result were not escaped as "\/". We know the exact request format from the platform's
- // InitDataParser.cpp, so we can use a regexp rather than parsing the JSON.
+ // Prior to O-MR1 the ClearKey CDM encoded the values in the "kids" array using Base64 encoding
+ // rather than Base64Url encoding. See [Internal: b/64388098]. We know the exact request format
+ // from the platform's InitDataParser.cpp. Since there aren't any "+" or "/" symbols elsewhere
+ // in the request, it's safe to fix the encoding by replacement through the whole request.
String requestString = Util.fromUtf8Bytes(request);
- Matcher requestKidsMatcher = REQUEST_KIDS_PATTERN.matcher(requestString);
- if (!requestKidsMatcher.find()) {
- Log.e(TAG, "Failed to adjust request data: " + requestString);
- return request;
- }
- int kidsStartIndex = requestKidsMatcher.start(1);
- int kidsEndIndex = requestKidsMatcher.end(1);
- StringBuilder adjustedRequestBuilder = new StringBuilder(requestString);
- base64ToBase64Url(adjustedRequestBuilder, kidsStartIndex, kidsEndIndex);
- return Util.getUtf8Bytes(adjustedRequestBuilder.toString());
+ return Util.getUtf8Bytes(base64ToBase64Url(requestString));
}
/**
@@ -71,39 +59,39 @@ import org.json.JSONObject;
return response;
}
// Prior to O-MR1 the ClearKey CDM expected Base64 encoding rather than Base64Url encoding for
- // the "k" and "kid" strings. See [Internal: b/64388098].
+ // the "k" and "kid" strings. See [Internal: b/64388098]. We know that the ClearKey CDM only
+ // looks at the k, kid and kty parameters in each key, so can ignore the rest of the response.
try {
JSONObject responseJson = new JSONObject(Util.fromUtf8Bytes(response));
+ StringBuilder adjustedResponseBuilder = new StringBuilder("{\"keys\":[");
JSONArray keysArray = responseJson.getJSONArray("keys");
for (int i = 0; i < keysArray.length(); i++) {
+ if (i != 0) {
+ adjustedResponseBuilder.append(",");
+ }
JSONObject key = keysArray.getJSONObject(i);
- key.put("k", base64UrlToBase64(key.getString("k")));
- key.put("kid", base64UrlToBase64(key.getString("kid")));
+ adjustedResponseBuilder.append("{\"k\":\"");
+ adjustedResponseBuilder.append(base64UrlToBase64(key.getString("k")));
+ adjustedResponseBuilder.append("\",\"kid\":\"");
+ adjustedResponseBuilder.append(base64UrlToBase64(key.getString("kid")));
+ adjustedResponseBuilder.append("\",\"kty\":\"");
+ adjustedResponseBuilder.append(key.getString("kty"));
+ adjustedResponseBuilder.append("\"}");
}
- return Util.getUtf8Bytes(responseJson.toString());
+ adjustedResponseBuilder.append("]}");
+ return Util.getUtf8Bytes(adjustedResponseBuilder.toString());
} catch (JSONException e) {
Log.e(TAG, "Failed to adjust response data: " + Util.fromUtf8Bytes(response), e);
return response;
}
}
- private static void base64ToBase64Url(StringBuilder base64, int startIndex, int endIndex) {
- for (int i = startIndex; i < endIndex; i++) {
- switch (base64.charAt(i)) {
- case '+':
- base64.setCharAt(i, '-');
- break;
- case '/':
- base64.setCharAt(i, '_');
- break;
- default:
- break;
- }
- }
+ private static String base64ToBase64Url(String base64) {
+ return base64.replace('+', '-').replace('/', '_');
}
- private static String base64UrlToBase64(String base64) {
- return base64.replace('-', '+').replace('_', '/');
+ private static String base64UrlToBase64(String base64Url) {
+ return base64Url.replace('-', '+').replace('_', '/');
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java
index 25fdaba5b8..c57b023139 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java
@@ -25,6 +25,7 @@ import android.os.Message;
import android.util.Log;
import android.util.Pair;
import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.drm.DefaultDrmSessionEventListener.EventDispatcher;
import com.google.android.exoplayer2.drm.ExoMediaDrm.DefaultKeyRequest;
import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest;
import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest;
@@ -80,8 +81,7 @@ import java.util.UUID;
private final String mimeType;
private final @DefaultDrmSessionManager.Mode int mode;
private final HashMap optionalKeyRequestParameters;
- private final Handler eventHandler;
- private final DefaultDrmSessionManager.EventListener eventListener;
+ private final EventDispatcher eventDispatcher;
private final int initialDrmRequestRetryCount;
/* package */ final MediaDrmCallback callback;
@@ -109,17 +109,22 @@ import java.util.UUID;
* @param optionalKeyRequestParameters The optional key request parameters.
* @param callback The media DRM callback.
* @param playbackLooper The playback looper.
- * @param eventHandler The handler to post listener events.
- * @param eventListener The DRM session manager event listener.
+ * @param eventDispatcher The dispatcher for DRM session manager events.
* @param initialDrmRequestRetryCount The number of times to retry for initial provisioning and
* key request before reporting error.
*/
- public DefaultDrmSession(UUID uuid, ExoMediaDrm mediaDrm,
- ProvisioningManager provisioningManager, byte[] initData, String mimeType,
- @DefaultDrmSessionManager.Mode int mode, byte[] offlineLicenseKeySetId,
- HashMap optionalKeyRequestParameters, MediaDrmCallback callback,
- Looper playbackLooper, Handler eventHandler,
- DefaultDrmSessionManager.EventListener eventListener,
+ public DefaultDrmSession(
+ UUID uuid,
+ ExoMediaDrm mediaDrm,
+ ProvisioningManager provisioningManager,
+ byte[] initData,
+ String mimeType,
+ @DefaultDrmSessionManager.Mode int mode,
+ byte[] offlineLicenseKeySetId,
+ HashMap optionalKeyRequestParameters,
+ MediaDrmCallback callback,
+ Looper playbackLooper,
+ EventDispatcher eventDispatcher,
int initialDrmRequestRetryCount) {
this.uuid = uuid;
this.provisioningManager = provisioningManager;
@@ -129,9 +134,7 @@ import java.util.UUID;
this.optionalKeyRequestParameters = optionalKeyRequestParameters;
this.callback = callback;
this.initialDrmRequestRetryCount = initialDrmRequestRetryCount;
-
- this.eventHandler = eventHandler;
- this.eventListener = eventListener;
+ this.eventDispatcher = eventDispatcher;
state = STATE_OPENING;
postResponseHandler = new PostResponseHandler(playbackLooper);
@@ -306,14 +309,7 @@ import java.util.UUID;
onError(new KeysExpiredException());
} else {
state = STATE_OPENED_WITH_KEYS;
- if (eventHandler != null && eventListener != null) {
- eventHandler.post(new Runnable() {
- @Override
- public void run() {
- eventListener.onDrmKeysRestored();
- }
- });
- }
+ eventDispatcher.drmKeysRestored();
}
}
break;
@@ -391,14 +387,7 @@ import java.util.UUID;
}
if (mode == DefaultDrmSessionManager.MODE_RELEASE) {
mediaDrm.provideKeyResponse(offlineLicenseKeySetId, responseData);
- if (eventHandler != null && eventListener != null) {
- eventHandler.post(new Runnable() {
- @Override
- public void run() {
- eventListener.onDrmKeysRemoved();
- }
- });
- }
+ eventDispatcher.drmKeysRemoved();
} else {
byte[] keySetId = mediaDrm.provideKeyResponse(sessionId, responseData);
if ((mode == DefaultDrmSessionManager.MODE_DOWNLOAD
@@ -407,14 +396,7 @@ import java.util.UUID;
offlineLicenseKeySetId = keySetId;
}
state = STATE_OPENED_WITH_KEYS;
- if (eventHandler != null && eventListener != null) {
- eventHandler.post(new Runnable() {
- @Override
- public void run() {
- eventListener.onDrmKeysLoaded();
- }
- });
- }
+ eventDispatcher.drmKeysLoaded();
}
} catch (Exception e) {
onKeysError(e);
@@ -438,14 +420,7 @@ import java.util.UUID;
private void onError(final Exception e) {
lastException = new DrmSessionException(e);
- if (eventHandler != null && eventListener != null) {
- eventHandler.post(new Runnable() {
- @Override
- public void run() {
- eventListener.onDrmSessionManagerError(e);
- }
- });
- }
+ eventDispatcher.drmSessionManagerError(e);
if (state != STATE_OPENED_WITH_KEYS) {
state = STATE_ERROR;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionEventListener.java
new file mode 100644
index 0000000000..7cdee7c537
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionEventListener.java
@@ -0,0 +1,141 @@
+/*
+ * 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.drm;
+
+import android.os.Handler;
+import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.util.Assertions;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/** Listener of {@link DefaultDrmSessionManager} events. */
+public interface DefaultDrmSessionEventListener {
+
+ /** Called each time keys are loaded. */
+ void onDrmKeysLoaded();
+
+ /**
+ * Called when a drm error occurs.
+ *
+ * This method being called does not indicate that playback has failed, or that it will fail.
+ * The player may be able to recover from the error and continue. Hence applications should
+ * not implement this method to display a user visible error or initiate an application
+ * level retry ({@link Player.EventListener#onPlayerError} is the appropriate place to implement
+ * such behavior). This method is called to provide the application with an opportunity to log the
+ * error if it wishes to do so.
+ *
+ * @param error The corresponding exception.
+ */
+ void onDrmSessionManagerError(Exception error);
+
+ /** Called each time offline keys are restored. */
+ void onDrmKeysRestored();
+
+ /** Called each time offline keys are removed. */
+ void onDrmKeysRemoved();
+
+ /** Dispatches drm events to all registered listeners. */
+ final class EventDispatcher {
+
+ private final CopyOnWriteArrayList listeners;
+
+ /** Creates event dispatcher. */
+ public EventDispatcher() {
+ listeners = new CopyOnWriteArrayList<>();
+ }
+
+ /** Adds listener to event dispatcher. */
+ public void addListener(Handler handler, DefaultDrmSessionEventListener eventListener) {
+ Assertions.checkArgument(handler != null && eventListener != null);
+ listeners.add(new HandlerAndListener(handler, eventListener));
+ }
+
+ /** Removes listener from event dispatcher. */
+ public void removeListener(DefaultDrmSessionEventListener eventListener) {
+ for (HandlerAndListener handlerAndListener : listeners) {
+ if (handlerAndListener.listener == eventListener) {
+ listeners.remove(handlerAndListener);
+ }
+ }
+ }
+
+ /** Dispatches {@link DefaultDrmSessionEventListener#onDrmKeysLoaded()}. */
+ public void drmKeysLoaded() {
+ for (HandlerAndListener handlerAndListener : listeners) {
+ final DefaultDrmSessionEventListener listener = handlerAndListener.listener;
+ handlerAndListener.handler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ listener.onDrmKeysLoaded();
+ }
+ });
+ }
+ }
+
+ /** Dispatches {@link DefaultDrmSessionEventListener#onDrmSessionManagerError(Exception)}. */
+ public void drmSessionManagerError(final Exception e) {
+ for (HandlerAndListener handlerAndListener : listeners) {
+ final DefaultDrmSessionEventListener listener = handlerAndListener.listener;
+ handlerAndListener.handler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ listener.onDrmSessionManagerError(e);
+ }
+ });
+ }
+ }
+
+ /** Dispatches {@link DefaultDrmSessionEventListener#onDrmKeysRestored()}. */
+ public void drmKeysRestored() {
+ for (HandlerAndListener handlerAndListener : listeners) {
+ final DefaultDrmSessionEventListener listener = handlerAndListener.listener;
+ handlerAndListener.handler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ listener.onDrmKeysRestored();
+ }
+ });
+ }
+ }
+
+ /** Dispatches {@link DefaultDrmSessionEventListener#onDrmKeysRemoved()}. */
+ public void drmKeysRemoved() {
+ for (HandlerAndListener handlerAndListener : listeners) {
+ final DefaultDrmSessionEventListener listener = handlerAndListener.listener;
+ handlerAndListener.handler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ listener.onDrmKeysRemoved();
+ }
+ });
+ }
+ }
+
+ private static final class HandlerAndListener {
+
+ public final Handler handler;
+ public final DefaultDrmSessionEventListener listener;
+
+ public HandlerAndListener(Handler handler, DefaultDrmSessionEventListener eventListener) {
+ this.handler = handler;
+ this.listener = eventListener;
+ }
+ }
+ }
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java
index ca0302cdca..66c9e5cde7 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java
@@ -25,8 +25,8 @@ import android.support.annotation.NonNull;
import android.text.TextUtils;
import android.util.Log;
import com.google.android.exoplayer2.C;
-import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.drm.DefaultDrmSession.ProvisioningManager;
+import com.google.android.exoplayer2.drm.DefaultDrmSessionEventListener.EventDispatcher;
import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException;
import com.google.android.exoplayer2.drm.ExoMediaDrm.OnEventListener;
@@ -48,41 +48,9 @@ import java.util.UUID;
public class DefaultDrmSessionManager implements DrmSessionManager,
ProvisioningManager {
- /**
- * Listener of {@link DefaultDrmSessionManager} events.
- */
- public interface EventListener {
-
- /**
- * Called each time keys are loaded.
- */
- void onDrmKeysLoaded();
-
- /**
- * Called when a drm error occurs.
- *
- * This method being called does not indicate that playback has failed, or that it will fail.
- * The player may be able to recover from the error and continue. Hence applications should
- * not implement this method to display a user visible error or initiate an application
- * level retry ({@link Player.EventListener#onPlayerError} is the appropriate place to implement
- * such behavior). This method is called to provide the application with an opportunity to log
- * the error if it wishes to do so.
- *
- * @param e The corresponding exception.
- */
- void onDrmSessionManagerError(Exception e);
-
- /**
- * Called each time offline keys are restored.
- */
- void onDrmKeysRestored();
-
- /**
- * Called each time offline keys are removed.
- */
- void onDrmKeysRemoved();
-
- }
+ /** @deprecated Use {@link DefaultDrmSessionEventListener}. */
+ @Deprecated
+ public interface EventListener extends DefaultDrmSessionEventListener {}
/**
* Signals that the {@link DrmInitData} passed to {@link #acquireSession} does not contain does
@@ -127,8 +95,7 @@ public class DefaultDrmSessionManager implements DrmSe
private final ExoMediaDrm mediaDrm;
private final MediaDrmCallback callback;
private final HashMap optionalKeyRequestParameters;
- private final Handler eventHandler;
- private final EventListener eventListener;
+ private final EventDispatcher eventDispatcher;
private final boolean multiSession;
private final int initialDrmRequestRetryCount;
@@ -141,40 +108,70 @@ public class DefaultDrmSessionManager implements DrmSe
/* package */ volatile MediaDrmHandler mediaDrmHandler;
+ /**
+ * @deprecated Use {@link #newWidevineInstance(MediaDrmCallback, HashMap)} and {@link
+ * #addListener(Handler, DefaultDrmSessionEventListener)}.
+ */
+ @Deprecated
+ public static DefaultDrmSessionManager newWidevineInstance(
+ MediaDrmCallback callback,
+ HashMap optionalKeyRequestParameters,
+ Handler eventHandler,
+ DefaultDrmSessionEventListener eventListener)
+ throws UnsupportedDrmException {
+ DefaultDrmSessionManager drmSessionManager =
+ newWidevineInstance(callback, optionalKeyRequestParameters);
+ if (eventHandler != null && eventListener != null) {
+ drmSessionManager.addListener(eventHandler, eventListener);
+ }
+ return drmSessionManager;
+ }
+
/**
* Instantiates a new instance using the Widevine scheme.
*
* @param callback Performs key and provisioning requests.
* @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument
* to {@link ExoMediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null.
- * @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.
* @throws UnsupportedDrmException If the specified DRM scheme is not supported.
*/
public static DefaultDrmSessionManager newWidevineInstance(
- MediaDrmCallback callback, HashMap optionalKeyRequestParameters,
- Handler eventHandler, EventListener eventListener) throws UnsupportedDrmException {
- return newFrameworkInstance(C.WIDEVINE_UUID, callback, optionalKeyRequestParameters,
- eventHandler, eventListener);
+ MediaDrmCallback callback, HashMap optionalKeyRequestParameters)
+ throws UnsupportedDrmException {
+ return newFrameworkInstance(C.WIDEVINE_UUID, callback, optionalKeyRequestParameters);
+ }
+
+ /**
+ * @deprecated Use {@link #newPlayReadyInstance(MediaDrmCallback, String)} and {@link
+ * #addListener(Handler, DefaultDrmSessionEventListener)}.
+ */
+ @Deprecated
+ public static DefaultDrmSessionManager newPlayReadyInstance(
+ MediaDrmCallback callback,
+ String customData,
+ Handler eventHandler,
+ DefaultDrmSessionEventListener eventListener)
+ throws UnsupportedDrmException {
+ DefaultDrmSessionManager drmSessionManager =
+ newPlayReadyInstance(callback, customData);
+ if (eventHandler != null && eventListener != null) {
+ drmSessionManager.addListener(eventHandler, eventListener);
+ }
+ return drmSessionManager;
}
/**
* Instantiates a new instance using the PlayReady scheme.
- *
- * Note that PlayReady is unsupported by most Android devices, with the exception of Android TV
+ *
+ *
Note that PlayReady is unsupported by most Android devices, with the exception of Android TV
* devices, which do provide support.
*
* @param callback Performs key and provisioning requests.
* @param customData Optional custom data to include in requests generated by the instance.
- * @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.
* @throws UnsupportedDrmException If the specified DRM scheme is not supported.
*/
public static DefaultDrmSessionManager newPlayReadyInstance(
- MediaDrmCallback callback, String customData, Handler eventHandler,
- EventListener eventListener) throws UnsupportedDrmException {
+ MediaDrmCallback callback, String customData) throws UnsupportedDrmException {
HashMap optionalKeyRequestParameters;
if (!TextUtils.isEmpty(customData)) {
optionalKeyRequestParameters = new HashMap<>();
@@ -182,8 +179,27 @@ public class DefaultDrmSessionManager implements DrmSe
} else {
optionalKeyRequestParameters = null;
}
- return newFrameworkInstance(C.PLAYREADY_UUID, callback, optionalKeyRequestParameters,
- eventHandler, eventListener);
+ return newFrameworkInstance(C.PLAYREADY_UUID, callback, optionalKeyRequestParameters);
+ }
+
+ /**
+ * @deprecated Use {@link #newFrameworkInstance(UUID, MediaDrmCallback, HashMap)} and {@link
+ * #addListener(Handler, DefaultDrmSessionEventListener)}.
+ */
+ @Deprecated
+ public static DefaultDrmSessionManager newFrameworkInstance(
+ UUID uuid,
+ MediaDrmCallback callback,
+ HashMap optionalKeyRequestParameters,
+ Handler eventHandler,
+ DefaultDrmSessionEventListener eventListener)
+ throws UnsupportedDrmException {
+ DefaultDrmSessionManager drmSessionManager =
+ newFrameworkInstance(uuid, callback, optionalKeyRequestParameters);
+ if (eventHandler != null && eventListener != null) {
+ drmSessionManager.addListener(eventHandler, eventListener);
+ }
+ return drmSessionManager;
}
/**
@@ -193,34 +209,76 @@ public class DefaultDrmSessionManager implements DrmSe
* @param callback Performs key and provisioning requests.
* @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument
* to {@link ExoMediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null.
- * @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.
* @throws UnsupportedDrmException If the specified DRM scheme is not supported.
*/
public static DefaultDrmSessionManager newFrameworkInstance(
- UUID uuid, MediaDrmCallback callback, HashMap optionalKeyRequestParameters,
- Handler eventHandler, EventListener eventListener) throws UnsupportedDrmException {
- return new DefaultDrmSessionManager<>(uuid, FrameworkMediaDrm.newInstance(uuid), callback,
- optionalKeyRequestParameters, eventHandler, eventListener, false,
+ UUID uuid, MediaDrmCallback callback, HashMap optionalKeyRequestParameters)
+ throws UnsupportedDrmException {
+ return new DefaultDrmSessionManager<>(
+ uuid,
+ FrameworkMediaDrm.newInstance(uuid),
+ callback,
+ optionalKeyRequestParameters,
+ /* multiSession= */ false,
INITIAL_DRM_REQUEST_RETRY_COUNT);
}
/**
- * @param uuid The UUID of the drm scheme.
- * @param mediaDrm An underlying {@link ExoMediaDrm} for use by the manager.
- * @param callback Performs key and provisioning requests.
- * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument
- * to {@link ExoMediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null.
- * @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.
+ * @deprecated Use {@link #DefaultDrmSessionManager(UUID, ExoMediaDrm, MediaDrmCallback, HashMap)}
+ * and {@link #addListener(Handler, DefaultDrmSessionEventListener)}.
*/
- public DefaultDrmSessionManager(UUID uuid, ExoMediaDrm mediaDrm, MediaDrmCallback callback,
- HashMap optionalKeyRequestParameters, Handler eventHandler,
- EventListener eventListener) {
- this(uuid, mediaDrm, callback, optionalKeyRequestParameters, eventHandler, eventListener,
- false, INITIAL_DRM_REQUEST_RETRY_COUNT);
+ @Deprecated
+ public DefaultDrmSessionManager(
+ UUID uuid,
+ ExoMediaDrm mediaDrm,
+ MediaDrmCallback callback,
+ HashMap optionalKeyRequestParameters,
+ Handler eventHandler,
+ DefaultDrmSessionEventListener eventListener) {
+ this(uuid, mediaDrm, callback, optionalKeyRequestParameters);
+ if (eventHandler != null && eventListener != null) {
+ addListener(eventHandler, eventListener);
+ }
+ }
+
+ /**
+ * @param uuid The UUID of the drm scheme.
+ * @param mediaDrm An underlying {@link ExoMediaDrm} for use by the manager.
+ * @param callback Performs key and provisioning requests.
+ * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument
+ * to {@link ExoMediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null.
+ */
+ public DefaultDrmSessionManager(
+ UUID uuid,
+ ExoMediaDrm mediaDrm,
+ MediaDrmCallback callback,
+ HashMap optionalKeyRequestParameters) {
+ this(
+ uuid,
+ mediaDrm,
+ callback,
+ optionalKeyRequestParameters,
+ /* multiSession= */ false,
+ INITIAL_DRM_REQUEST_RETRY_COUNT);
+ }
+
+ /**
+ * @deprecated Use {@link #DefaultDrmSessionManager(UUID, ExoMediaDrm, MediaDrmCallback, HashMap,
+ * boolean)} and {@link #addListener(Handler, DefaultDrmSessionEventListener)}.
+ */
+ @Deprecated
+ public DefaultDrmSessionManager(
+ UUID uuid,
+ ExoMediaDrm mediaDrm,
+ MediaDrmCallback callback,
+ HashMap optionalKeyRequestParameters,
+ Handler eventHandler,
+ DefaultDrmSessionEventListener eventListener,
+ boolean multiSession) {
+ this(uuid, mediaDrm, callback, optionalKeyRequestParameters, multiSession);
+ if (eventHandler != null && eventListener != null) {
+ addListener(eventHandler, eventListener);
+ }
}
/**
@@ -229,17 +287,48 @@ public class DefaultDrmSessionManager implements DrmSe
* @param callback Performs key and provisioning requests.
* @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument
* to {@link ExoMediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null.
- * @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.
* @param multiSession A boolean that specify whether multiple key session support is enabled.
* Default is false.
*/
- public DefaultDrmSessionManager(UUID uuid, ExoMediaDrm mediaDrm, MediaDrmCallback callback,
- HashMap optionalKeyRequestParameters, Handler eventHandler,
- EventListener eventListener, boolean multiSession) {
- this(uuid, mediaDrm, callback, optionalKeyRequestParameters, eventHandler, eventListener,
- multiSession, INITIAL_DRM_REQUEST_RETRY_COUNT);
+ public DefaultDrmSessionManager(
+ UUID uuid,
+ ExoMediaDrm mediaDrm,
+ MediaDrmCallback callback,
+ HashMap optionalKeyRequestParameters,
+ boolean multiSession) {
+ this(
+ uuid,
+ mediaDrm,
+ callback,
+ optionalKeyRequestParameters,
+ multiSession,
+ INITIAL_DRM_REQUEST_RETRY_COUNT);
+ }
+
+ /**
+ * @deprecated Use {@link #DefaultDrmSessionManager(UUID, ExoMediaDrm, MediaDrmCallback, HashMap,
+ * boolean, int)} and {@link #addListener(Handler, DefaultDrmSessionEventListener)}.
+ */
+ @Deprecated
+ public DefaultDrmSessionManager(
+ UUID uuid,
+ ExoMediaDrm mediaDrm,
+ MediaDrmCallback callback,
+ HashMap optionalKeyRequestParameters,
+ Handler eventHandler,
+ DefaultDrmSessionEventListener eventListener,
+ boolean multiSession,
+ int initialDrmRequestRetryCount) {
+ this(
+ uuid,
+ mediaDrm,
+ callback,
+ optionalKeyRequestParameters,
+ multiSession,
+ initialDrmRequestRetryCount);
+ if (eventHandler != null && eventListener != null) {
+ addListener(eventHandler, eventListener);
+ }
}
/**
@@ -248,17 +337,18 @@ public class DefaultDrmSessionManager implements DrmSe
* @param callback Performs key and provisioning requests.
* @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument
* to {@link ExoMediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null.
- * @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.
* @param multiSession A boolean that specify whether multiple key session support is enabled.
* Default is false.
* @param initialDrmRequestRetryCount The number of times to retry for initial provisioning and
* key request before reporting error.
*/
- public DefaultDrmSessionManager(UUID uuid, ExoMediaDrm mediaDrm, MediaDrmCallback callback,
- HashMap optionalKeyRequestParameters, Handler eventHandler,
- EventListener eventListener, boolean multiSession, int initialDrmRequestRetryCount) {
+ public DefaultDrmSessionManager(
+ UUID uuid,
+ ExoMediaDrm mediaDrm,
+ MediaDrmCallback callback,
+ HashMap optionalKeyRequestParameters,
+ boolean multiSession,
+ int initialDrmRequestRetryCount) {
Assertions.checkNotNull(uuid);
Assertions.checkNotNull(mediaDrm);
Assertions.checkArgument(!C.COMMON_PSSH_UUID.equals(uuid), "Use C.CLEARKEY_UUID instead");
@@ -266,8 +356,7 @@ public class DefaultDrmSessionManager implements DrmSe
this.mediaDrm = mediaDrm;
this.callback = callback;
this.optionalKeyRequestParameters = optionalKeyRequestParameters;
- this.eventHandler = eventHandler;
- this.eventListener = eventListener;
+ this.eventDispatcher = new EventDispatcher();
this.multiSession = multiSession;
this.initialDrmRequestRetryCount = initialDrmRequestRetryCount;
mode = MODE_PLAYBACK;
@@ -279,6 +368,25 @@ public class DefaultDrmSessionManager implements DrmSe
mediaDrm.setOnEventListener(new MediaDrmEventListener());
}
+ /**
+ * Adds a {@link DefaultDrmSessionEventListener} to listen to drm session events.
+ *
+ * @param handler A handler to use when delivering events to {@code eventListener}.
+ * @param eventListener A listener of events.
+ */
+ public final void addListener(Handler handler, DefaultDrmSessionEventListener eventListener) {
+ eventDispatcher.addListener(handler, eventListener);
+ }
+
+ /**
+ * Removes a {@link DefaultDrmSessionEventListener} from the list of drm session event listeners.
+ *
+ * @param eventListener The listener to remove.
+ */
+ public final void removeListener(DefaultDrmSessionEventListener eventListener) {
+ eventDispatcher.removeListener(eventListener);
+ }
+
/**
* Provides access to {@link ExoMediaDrm#getPropertyString(String)}.
*
@@ -383,8 +491,9 @@ public class DefaultDrmSessionManager implements DrmSe
return true;
} else if (C.CENC_TYPE_cbc1.equals(schemeType) || C.CENC_TYPE_cbcs.equals(schemeType)
|| C.CENC_TYPE_cens.equals(schemeType)) {
- // AES-CBC and pattern encryption are supported on API 24 onwards.
- return Util.SDK_INT >= 24;
+ // API support for AES-CBC and pattern encryption was added in API 24. However, the
+ // implementation was not stable until API 25.
+ return Util.SDK_INT >= 25;
}
// Unknown schemes, assume one of them is supported.
return true;
@@ -406,15 +515,7 @@ public class DefaultDrmSessionManager implements DrmSe
SchemeData data = getSchemeData(drmInitData, uuid, false);
if (data == null) {
final MissingSchemeDataException error = new MissingSchemeDataException(uuid);
- if (eventHandler != null && eventListener != null) {
- eventHandler.post(
- new Runnable() {
- @Override
- public void run() {
- eventListener.onDrmSessionManagerError(error);
- }
- });
- }
+ eventDispatcher.drmSessionManagerError(error);
return new ErrorStateDrmSession<>(new DrmSessionException(error));
}
initData = getSchemeInitData(data, uuid);
@@ -437,9 +538,20 @@ public class DefaultDrmSessionManager implements DrmSe
if (session == null) {
// Create a new session.
- session = new DefaultDrmSession<>(uuid, mediaDrm, this, initData, mimeType, mode,
- offlineLicenseKeySetId, optionalKeyRequestParameters, callback, playbackLooper,
- eventHandler, eventListener, initialDrmRequestRetryCount);
+ session =
+ new DefaultDrmSession<>(
+ uuid,
+ mediaDrm,
+ this,
+ initData,
+ mimeType,
+ mode,
+ offlineLicenseKeySetId,
+ optionalKeyRequestParameters,
+ callback,
+ playbackLooper,
+ eventDispatcher,
+ initialDrmRequestRetryCount);
sessions.add(session);
}
session.acquire();
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java
index 0c7cb0ef01..4a59667dc8 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java
@@ -39,18 +39,13 @@ public final class DrmInitData implements Comparator, Parcelable {
* The result is generated as follows.
*
*
- * -
- * Include all {@link SchemeData}s from {@code manifestData} where {@link
- * SchemeData#hasData()} is true.
- *
- * -
- * Include all {@link SchemeData}s in {@code mediaData} where {@link SchemeData#hasData()} is
- * true and for which we did not include an entry from the manifest targeting the same UUID.
- *
- * -
- * If available, the scheme type from the manifest is used. If not, the scheme type from the
- * media is used.
- *
+ * - Include all {@link SchemeData}s from {@code manifestData} where {@link
+ * SchemeData#hasData()} is true.
+ *
- Include all {@link SchemeData}s in {@code mediaData} where {@link SchemeData#hasData()}
+ * is true and for which we did not include an entry from the manifest targeting the same
+ * UUID.
+ *
- If available, the scheme type from the manifest is used. If not, the scheme type from the
+ * media is used.
*
*
* @param manifestData DRM session acquisition data obtained from the manifest.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/ErrorStateDrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/ErrorStateDrmSession.java
index 576f0a08a9..d30e670c3f 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/drm/ErrorStateDrmSession.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/ErrorStateDrmSession.java
@@ -18,10 +18,8 @@ package com.google.android.exoplayer2.drm;
import com.google.android.exoplayer2.util.Assertions;
import java.util.Map;
-/**
- * A {@link DrmSession} that's in a terminal error state.
- */
-/* package */ final class ErrorStateDrmSession implements DrmSession {
+/** A {@link DrmSession} that's in a terminal error state. */
+public final class ErrorStateDrmSession implements DrmSession {
private final DrmSessionException error;
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/drm/HttpMediaDrmCallback.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java
index dfbf3dee07..4a93ac8333 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java
@@ -24,10 +24,12 @@ import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest;
import com.google.android.exoplayer2.upstream.DataSourceInputStream;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.HttpDataSource;
+import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidResponseCodeException;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.UUID;
@@ -37,6 +39,8 @@ import java.util.UUID;
@TargetApi(18)
public final class HttpMediaDrmCallback implements MediaDrmCallback {
+ private static final int MAX_MANUAL_REDIRECTS = 5;
+
private final HttpDataSource.Factory dataSourceFactory;
private final String defaultLicenseUrl;
private final boolean forceDefaultLicenseUrl;
@@ -138,14 +142,46 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback {
dataSource.setRequestProperty(requestProperty.getKey(), requestProperty.getValue());
}
}
- DataSpec dataSpec = new DataSpec(Uri.parse(url), data, 0, 0, C.LENGTH_UNSET, null,
- DataSpec.FLAG_ALLOW_GZIP);
- DataSourceInputStream inputStream = new DataSourceInputStream(dataSource, dataSpec);
- try {
- return Util.toByteArray(inputStream);
- } finally {
- Util.closeQuietly(inputStream);
+
+ int manualRedirectCount = 0;
+ while (true) {
+ DataSpec dataSpec =
+ new DataSpec(
+ Uri.parse(url),
+ data,
+ /* absoluteStreamPosition= */ 0,
+ /* position= */ 0,
+ /* length= */ C.LENGTH_UNSET,
+ /* key= */ null,
+ DataSpec.FLAG_ALLOW_GZIP);
+ DataSourceInputStream inputStream = new DataSourceInputStream(dataSource, dataSpec);
+ try {
+ return Util.toByteArray(inputStream);
+ } catch (InvalidResponseCodeException e) {
+ // For POST requests, the underlying network stack will not normally follow 307 or 308
+ // redirects automatically. Do so manually here.
+ boolean manuallyRedirect =
+ (e.responseCode == 307 || e.responseCode == 308)
+ && manualRedirectCount++ < MAX_MANUAL_REDIRECTS;
+ url = manuallyRedirect ? getRedirectUrl(e) : null;
+ if (url == null) {
+ throw e;
+ }
+ } finally {
+ Util.closeQuietly(inputStream);
+ }
}
}
+ private static String getRedirectUrl(InvalidResponseCodeException exception) {
+ Map> headerFields = exception.headerFields;
+ if (headerFields != null) {
+ List locationHeaders = headerFields.get("Location");
+ if (locationHeaders != null && !locationHeaders.isEmpty()) {
+ return locationHeaders.get(0);
+ }
+ }
+ return null;
+ }
+
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java
index 481bea66c3..9298c16cb0 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java
@@ -21,7 +21,6 @@ import android.os.Handler;
import android.os.HandlerThread;
import android.util.Pair;
import com.google.android.exoplayer2.C;
-import com.google.android.exoplayer2.drm.DefaultDrmSessionManager.EventListener;
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager.Mode;
import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException;
import com.google.android.exoplayer2.upstream.HttpDataSource;
@@ -90,10 +89,12 @@ public final class OfflineLicenseHelper {
* @throws UnsupportedDrmException If the Widevine DRM scheme is unsupported or cannot be
* instantiated.
* @see DefaultDrmSessionManager#DefaultDrmSessionManager(java.util.UUID, ExoMediaDrm,
- * MediaDrmCallback, HashMap, Handler, EventListener)
+ * MediaDrmCallback, HashMap, Handler, DefaultDrmSessionEventListener)
*/
public static OfflineLicenseHelper newWidevineInstance(
- String defaultLicenseUrl, boolean forceDefaultLicenseUrl, Factory httpDataSourceFactory,
+ String defaultLicenseUrl,
+ boolean forceDefaultLicenseUrl,
+ Factory httpDataSourceFactory,
HashMap optionalKeyRequestParameters)
throws UnsupportedDrmException {
return new OfflineLicenseHelper<>(C.WIDEVINE_UUID,
@@ -111,36 +112,41 @@ public final class OfflineLicenseHelper {
* @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument
* to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null.
* @see DefaultDrmSessionManager#DefaultDrmSessionManager(java.util.UUID, ExoMediaDrm,
- * MediaDrmCallback, HashMap, Handler, EventListener)
+ * MediaDrmCallback, HashMap, Handler, DefaultDrmSessionEventListener)
*/
- public OfflineLicenseHelper(UUID uuid, ExoMediaDrm mediaDrm, MediaDrmCallback callback,
+ public OfflineLicenseHelper(
+ UUID uuid,
+ ExoMediaDrm mediaDrm,
+ MediaDrmCallback callback,
HashMap optionalKeyRequestParameters) {
handlerThread = new HandlerThread("OfflineLicenseHelper");
handlerThread.start();
conditionVariable = new ConditionVariable();
- EventListener eventListener = new EventListener() {
- @Override
- public void onDrmKeysLoaded() {
- conditionVariable.open();
- }
+ DefaultDrmSessionEventListener eventListener =
+ new DefaultDrmSessionEventListener() {
+ @Override
+ public void onDrmKeysLoaded() {
+ conditionVariable.open();
+ }
- @Override
- public void onDrmSessionManagerError(Exception e) {
- conditionVariable.open();
- }
+ @Override
+ public void onDrmSessionManagerError(Exception e) {
+ conditionVariable.open();
+ }
- @Override
- public void onDrmKeysRestored() {
- conditionVariable.open();
- }
+ @Override
+ public void onDrmKeysRestored() {
+ conditionVariable.open();
+ }
- @Override
- public void onDrmKeysRemoved() {
- conditionVariable.open();
- }
- };
- drmSessionManager = new DefaultDrmSessionManager<>(uuid, mediaDrm, callback,
- optionalKeyRequestParameters, new Handler(handlerThread.getLooper()), eventListener);
+ @Override
+ public void onDrmKeysRemoved() {
+ conditionVariable.open();
+ }
+ };
+ drmSessionManager =
+ new DefaultDrmSessionManager<>(uuid, mediaDrm, callback, optionalKeyRequestParameters);
+ drmSessionManager.addListener(new Handler(handlerThread.getLooper()), eventListener);
}
/**
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/Id3Peeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/Id3Peeker.java
new file mode 100644
index 0000000000..8dbcfafaf2
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/Id3Peeker.java
@@ -0,0 +1,87 @@
+/*
+ * 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;
+
+import android.support.annotation.Nullable;
+import com.google.android.exoplayer2.metadata.Metadata;
+import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.EOFException;
+import java.io.IOException;
+
+/**
+ * Peeks data from the beginning of an {@link ExtractorInput} to determine if there is any ID3 tag.
+ */
+public final class Id3Peeker {
+
+ private final ParsableByteArray scratch;
+
+ public Id3Peeker() {
+ scratch = new ParsableByteArray(Id3Decoder.ID3_HEADER_LENGTH);
+ }
+
+ /**
+ * Peeks ID3 data from the input and parses the first ID3 tag.
+ *
+ * @param input The {@link ExtractorInput} from which data should be peeked.
+ * @param id3FramePredicate Determines which ID3 frames are decoded. May be null to decode all
+ * frames.
+ * @return The first ID3 tag decoded into a {@link Metadata} object. May be null if ID3 tag is not
+ * present in the input.
+ * @throws IOException If an error occurred peeking from the input.
+ * @throws InterruptedException If the thread was interrupted.
+ */
+ @Nullable
+ public Metadata peekId3Data(
+ ExtractorInput input, @Nullable Id3Decoder.FramePredicate id3FramePredicate)
+ throws IOException, InterruptedException {
+ int peekedId3Bytes = 0;
+ Metadata metadata = null;
+ while (true) {
+ try {
+ input.peekFully(scratch.data, 0, Id3Decoder.ID3_HEADER_LENGTH);
+ } catch (EOFException e) {
+ // If input has less than ID3_HEADER_LENGTH, ignore the rest.
+ break;
+ }
+ scratch.setPosition(0);
+ if (scratch.readUnsignedInt24() != Id3Decoder.ID3_TAG) {
+ // Not an ID3 tag.
+ break;
+ }
+ scratch.skipBytes(3); // Skip major version, minor version and flags.
+ int framesLength = scratch.readSynchSafeInt();
+ int tagLength = Id3Decoder.ID3_HEADER_LENGTH + framesLength;
+
+ if (metadata == null) {
+ byte[] id3Data = new byte[tagLength];
+ System.arraycopy(scratch.data, 0, id3Data, 0, Id3Decoder.ID3_HEADER_LENGTH);
+ input.peekFully(id3Data, Id3Decoder.ID3_HEADER_LENGTH, framesLength);
+
+ metadata = new Id3Decoder(id3FramePredicate).decode(id3Data, tagLength);
+ } else {
+ input.advancePeekPosition(framesLength);
+ }
+
+ peekedId3Bytes += tagLength;
+ }
+
+ input.resetPeekPosition();
+ input.advancePeekPosition(peekedId3Bytes);
+ return metadata;
+ }
+}
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/mkv/DefaultEbmlReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java
index 2c6130677f..21cb3775e5 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java
@@ -15,12 +15,15 @@
*/
package com.google.android.exoplayer2.extractor.mkv;
+import android.support.annotation.IntDef;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.util.Assertions;
import java.io.EOFException;
import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.Stack;
/**
@@ -28,6 +31,10 @@ import java.util.Stack;
*/
/* package */ final class DefaultEbmlReader implements EbmlReader {
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({ELEMENT_STATE_READ_ID, ELEMENT_STATE_READ_CONTENT_SIZE, ELEMENT_STATE_READ_CONTENT})
+ private @interface ElementState {}
+
private static final int ELEMENT_STATE_READ_ID = 0;
private static final int ELEMENT_STATE_READ_CONTENT_SIZE = 1;
private static final int ELEMENT_STATE_READ_CONTENT = 2;
@@ -44,7 +51,7 @@ import java.util.Stack;
private final VarintReader varintReader = new VarintReader();
private EbmlReaderOutput output;
- private int elementState;
+ private @ElementState int elementState;
private int elementId;
private long elementContentSize;
@@ -88,23 +95,23 @@ import java.util.Stack;
elementState = ELEMENT_STATE_READ_CONTENT;
}
- int type = output.getElementType(elementId);
+ @EbmlReaderOutput.ElementType int type = output.getElementType(elementId);
switch (type) {
- case TYPE_MASTER:
+ case EbmlReaderOutput.TYPE_MASTER:
long elementContentPosition = input.getPosition();
long elementEndPosition = elementContentPosition + elementContentSize;
masterElementsStack.add(new MasterElement(elementId, elementEndPosition));
output.startMasterElement(elementId, elementContentPosition, elementContentSize);
elementState = ELEMENT_STATE_READ_ID;
return true;
- case TYPE_UNSIGNED_INT:
+ case EbmlReaderOutput.TYPE_UNSIGNED_INT:
if (elementContentSize > MAX_INTEGER_ELEMENT_SIZE_BYTES) {
throw new ParserException("Invalid integer size: " + elementContentSize);
}
output.integerElement(elementId, readInteger(input, (int) elementContentSize));
elementState = ELEMENT_STATE_READ_ID;
return true;
- case TYPE_FLOAT:
+ case EbmlReaderOutput.TYPE_FLOAT:
if (elementContentSize != VALID_FLOAT32_ELEMENT_SIZE_BYTES
&& elementContentSize != VALID_FLOAT64_ELEMENT_SIZE_BYTES) {
throw new ParserException("Invalid float size: " + elementContentSize);
@@ -112,18 +119,18 @@ import java.util.Stack;
output.floatElement(elementId, readFloat(input, (int) elementContentSize));
elementState = ELEMENT_STATE_READ_ID;
return true;
- case TYPE_STRING:
+ case EbmlReaderOutput.TYPE_STRING:
if (elementContentSize > Integer.MAX_VALUE) {
throw new ParserException("String element size: " + elementContentSize);
}
output.stringElement(elementId, readString(input, (int) elementContentSize));
elementState = ELEMENT_STATE_READ_ID;
return true;
- case TYPE_BINARY:
+ case EbmlReaderOutput.TYPE_BINARY:
output.binaryElement(elementId, (int) elementContentSize, input);
elementState = ELEMENT_STATE_READ_ID;
return true;
- case TYPE_UNKNOWN:
+ case EbmlReaderOutput.TYPE_UNKNOWN:
input.skipFully((int) elementContentSize);
elementState = ELEMENT_STATE_READ_ID;
break;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/EbmlReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/EbmlReader.java
index dc059d2cc8..9987b3c8e6 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/EbmlReader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/EbmlReader.java
@@ -28,31 +28,6 @@ import java.io.IOException;
*/
/* package */ interface EbmlReader {
- /**
- * Type for unknown elements.
- */
- int TYPE_UNKNOWN = 0;
- /**
- * Type for elements that contain child elements.
- */
- int TYPE_MASTER = 1;
- /**
- * Type for integer value elements of up to 8 bytes.
- */
- int TYPE_UNSIGNED_INT = 2;
- /**
- * Type for string elements.
- */
- int TYPE_STRING = 3;
- /**
- * Type for binary elements.
- */
- int TYPE_BINARY = 4;
- /**
- * Type for IEEE floating point value elements of either 4 or 8 bytes.
- */
- int TYPE_FLOAT = 5;
-
/**
* Initializes the extractor with an {@link EbmlReaderOutput}.
*
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/EbmlReaderOutput.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/EbmlReaderOutput.java
index 6c97e802b9..b1cd508c8e 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/EbmlReaderOutput.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/EbmlReaderOutput.java
@@ -15,24 +15,46 @@
*/
package com.google.android.exoplayer2.extractor.mkv;
+import android.support.annotation.IntDef;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
/**
* Defines EBML element IDs/types and reacts to events.
*/
/* package */ interface EbmlReaderOutput {
+ /** EBML element types. */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({TYPE_UNKNOWN, TYPE_MASTER, TYPE_UNSIGNED_INT, TYPE_STRING, TYPE_BINARY, TYPE_FLOAT})
+ @interface ElementType {}
+ /** Type for unknown elements. */
+ int TYPE_UNKNOWN = 0;
+ /** Type for elements that contain child elements. */
+ int TYPE_MASTER = 1;
+ /** Type for integer value elements of up to 8 bytes. */
+ int TYPE_UNSIGNED_INT = 2;
+ /** Type for string elements. */
+ int TYPE_STRING = 3;
+ /** Type for binary elements. */
+ int TYPE_BINARY = 4;
+ /** Type for IEEE floating point value elements of either 4 or 8 bytes. */
+ int TYPE_FLOAT = 5;
+
/**
* Maps an element ID to a corresponding type.
- *
- * If {@link EbmlReader#TYPE_UNKNOWN} is returned then the element is skipped. Note that all
- * children of a skipped element are also skipped.
+ *
+ *
If {@link #TYPE_UNKNOWN} is returned then the element is skipped. Note that all children of
+ * a skipped element are also skipped.
*
* @param id The element ID to map.
- * @return One of the {@code TYPE_} constants defined in {@link EbmlReader}.
+ * @return One of {@link #TYPE_UNKNOWN}, {@link #TYPE_MASTER}, {@link #TYPE_UNSIGNED_INT}, {@link
+ * #TYPE_STRING}, {@link #TYPE_BINARY} and {@link #TYPE_FLOAT}.
*/
+ @ElementType
int getElementType(int id);
/**
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java
index 57128f45f0..1049554f7a 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java
@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.mkv;
import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import android.util.Log;
+import android.util.Pair;
import android.util.SparseArray;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
@@ -219,6 +220,7 @@ public final class MatroskaExtractor implements Extractor {
private static final int LACING_EBML = 3;
private static final int FOURCC_COMPRESSION_VC1 = 0x31435657;
+ private static final int FOURCC_COMPRESSION_DIVX = 0x58564944;
/**
* A template for the prefix that must be added to each subrip sample. The 12 byte end timecode
@@ -446,100 +448,6 @@ public final class MatroskaExtractor implements Extractor {
return Extractor.RESULT_CONTINUE;
}
- /* package */ int getElementType(int id) {
- switch (id) {
- case ID_EBML:
- case ID_SEGMENT:
- case ID_SEEK_HEAD:
- case ID_SEEK:
- case ID_INFO:
- case ID_CLUSTER:
- case ID_TRACKS:
- case ID_TRACK_ENTRY:
- case ID_AUDIO:
- case ID_VIDEO:
- case ID_CONTENT_ENCODINGS:
- case ID_CONTENT_ENCODING:
- case ID_CONTENT_COMPRESSION:
- case ID_CONTENT_ENCRYPTION:
- case ID_CONTENT_ENCRYPTION_AES_SETTINGS:
- case ID_CUES:
- case ID_CUE_POINT:
- case ID_CUE_TRACK_POSITIONS:
- case ID_BLOCK_GROUP:
- case ID_PROJECTION:
- case ID_COLOUR:
- case ID_MASTERING_METADATA:
- return EbmlReader.TYPE_MASTER;
- case ID_EBML_READ_VERSION:
- case ID_DOC_TYPE_READ_VERSION:
- case ID_SEEK_POSITION:
- case ID_TIMECODE_SCALE:
- case ID_TIME_CODE:
- case ID_BLOCK_DURATION:
- case ID_PIXEL_WIDTH:
- case ID_PIXEL_HEIGHT:
- case ID_DISPLAY_WIDTH:
- case ID_DISPLAY_HEIGHT:
- case ID_DISPLAY_UNIT:
- case ID_TRACK_NUMBER:
- case ID_TRACK_TYPE:
- case ID_FLAG_DEFAULT:
- case ID_FLAG_FORCED:
- case ID_DEFAULT_DURATION:
- case ID_CODEC_DELAY:
- case ID_SEEK_PRE_ROLL:
- case ID_CHANNELS:
- case ID_AUDIO_BIT_DEPTH:
- case ID_CONTENT_ENCODING_ORDER:
- case ID_CONTENT_ENCODING_SCOPE:
- case ID_CONTENT_COMPRESSION_ALGORITHM:
- case ID_CONTENT_ENCRYPTION_ALGORITHM:
- case ID_CONTENT_ENCRYPTION_AES_SETTINGS_CIPHER_MODE:
- case ID_CUE_TIME:
- case ID_CUE_CLUSTER_POSITION:
- case ID_REFERENCE_BLOCK:
- case ID_STEREO_MODE:
- case ID_COLOUR_RANGE:
- case ID_COLOUR_TRANSFER:
- case ID_COLOUR_PRIMARIES:
- case ID_MAX_CLL:
- case ID_MAX_FALL:
- return EbmlReader.TYPE_UNSIGNED_INT;
- case ID_DOC_TYPE:
- case ID_CODEC_ID:
- case ID_LANGUAGE:
- return EbmlReader.TYPE_STRING;
- case ID_SEEK_ID:
- case ID_CONTENT_COMPRESSION_SETTINGS:
- case ID_CONTENT_ENCRYPTION_KEY_ID:
- case ID_SIMPLE_BLOCK:
- case ID_BLOCK:
- case ID_CODEC_PRIVATE:
- case ID_PROJECTION_PRIVATE:
- return EbmlReader.TYPE_BINARY;
- case ID_DURATION:
- case ID_SAMPLING_FREQUENCY:
- case ID_PRIMARY_R_CHROMATICITY_X:
- case ID_PRIMARY_R_CHROMATICITY_Y:
- case ID_PRIMARY_G_CHROMATICITY_X:
- case ID_PRIMARY_G_CHROMATICITY_Y:
- case ID_PRIMARY_B_CHROMATICITY_X:
- case ID_PRIMARY_B_CHROMATICITY_Y:
- case ID_WHITE_POINT_CHROMATICITY_X:
- case ID_WHITE_POINT_CHROMATICITY_Y:
- case ID_LUMNINANCE_MAX:
- case ID_LUMNINANCE_MIN:
- return EbmlReader.TYPE_FLOAT;
- default:
- return EbmlReader.TYPE_UNKNOWN;
- }
- }
-
- /* package */ boolean isLevel1Element(int id) {
- return id == ID_SEGMENT_INFO || id == ID_CLUSTER || id == ID_CUES || id == ID_TRACKS;
- }
-
/* package */ void startMasterElement(int id, long contentPosition, long contentSize)
throws ParserException {
switch (id) {
@@ -1499,12 +1407,98 @@ public final class MatroskaExtractor implements Extractor {
@Override
public int getElementType(int id) {
- return MatroskaExtractor.this.getElementType(id);
+ switch (id) {
+ case ID_EBML:
+ case ID_SEGMENT:
+ case ID_SEEK_HEAD:
+ case ID_SEEK:
+ case ID_INFO:
+ case ID_CLUSTER:
+ case ID_TRACKS:
+ case ID_TRACK_ENTRY:
+ case ID_AUDIO:
+ case ID_VIDEO:
+ case ID_CONTENT_ENCODINGS:
+ case ID_CONTENT_ENCODING:
+ case ID_CONTENT_COMPRESSION:
+ case ID_CONTENT_ENCRYPTION:
+ case ID_CONTENT_ENCRYPTION_AES_SETTINGS:
+ case ID_CUES:
+ case ID_CUE_POINT:
+ case ID_CUE_TRACK_POSITIONS:
+ case ID_BLOCK_GROUP:
+ case ID_PROJECTION:
+ case ID_COLOUR:
+ case ID_MASTERING_METADATA:
+ return TYPE_MASTER;
+ case ID_EBML_READ_VERSION:
+ case ID_DOC_TYPE_READ_VERSION:
+ case ID_SEEK_POSITION:
+ case ID_TIMECODE_SCALE:
+ case ID_TIME_CODE:
+ case ID_BLOCK_DURATION:
+ case ID_PIXEL_WIDTH:
+ case ID_PIXEL_HEIGHT:
+ case ID_DISPLAY_WIDTH:
+ case ID_DISPLAY_HEIGHT:
+ case ID_DISPLAY_UNIT:
+ case ID_TRACK_NUMBER:
+ case ID_TRACK_TYPE:
+ case ID_FLAG_DEFAULT:
+ case ID_FLAG_FORCED:
+ case ID_DEFAULT_DURATION:
+ case ID_CODEC_DELAY:
+ case ID_SEEK_PRE_ROLL:
+ case ID_CHANNELS:
+ case ID_AUDIO_BIT_DEPTH:
+ case ID_CONTENT_ENCODING_ORDER:
+ case ID_CONTENT_ENCODING_SCOPE:
+ case ID_CONTENT_COMPRESSION_ALGORITHM:
+ case ID_CONTENT_ENCRYPTION_ALGORITHM:
+ case ID_CONTENT_ENCRYPTION_AES_SETTINGS_CIPHER_MODE:
+ case ID_CUE_TIME:
+ case ID_CUE_CLUSTER_POSITION:
+ case ID_REFERENCE_BLOCK:
+ case ID_STEREO_MODE:
+ case ID_COLOUR_RANGE:
+ case ID_COLOUR_TRANSFER:
+ case ID_COLOUR_PRIMARIES:
+ case ID_MAX_CLL:
+ case ID_MAX_FALL:
+ return TYPE_UNSIGNED_INT;
+ case ID_DOC_TYPE:
+ case ID_CODEC_ID:
+ case ID_LANGUAGE:
+ return TYPE_STRING;
+ case ID_SEEK_ID:
+ case ID_CONTENT_COMPRESSION_SETTINGS:
+ case ID_CONTENT_ENCRYPTION_KEY_ID:
+ case ID_SIMPLE_BLOCK:
+ case ID_BLOCK:
+ case ID_CODEC_PRIVATE:
+ case ID_PROJECTION_PRIVATE:
+ return TYPE_BINARY;
+ case ID_DURATION:
+ case ID_SAMPLING_FREQUENCY:
+ case ID_PRIMARY_R_CHROMATICITY_X:
+ case ID_PRIMARY_R_CHROMATICITY_Y:
+ case ID_PRIMARY_G_CHROMATICITY_X:
+ case ID_PRIMARY_G_CHROMATICITY_Y:
+ case ID_PRIMARY_B_CHROMATICITY_X:
+ case ID_PRIMARY_B_CHROMATICITY_Y:
+ case ID_WHITE_POINT_CHROMATICITY_X:
+ case ID_WHITE_POINT_CHROMATICITY_Y:
+ case ID_LUMNINANCE_MAX:
+ case ID_LUMNINANCE_MIN:
+ return TYPE_FLOAT;
+ default:
+ return TYPE_UNKNOWN;
+ }
}
@Override
public boolean isLevel1Element(int id) {
- return MatroskaExtractor.this.isLevel1Element(id);
+ return id == ID_SEGMENT_INFO || id == ID_CLUSTER || id == ID_CUES || id == ID_TRACKS;
}
@Override
@@ -1711,13 +1705,9 @@ public final class MatroskaExtractor implements Extractor {
nalUnitLengthFieldLength = hevcConfig.nalUnitLengthFieldLength;
break;
case CODEC_ID_FOURCC:
- initializationData = parseFourCcVc1Private(new ParsableByteArray(codecPrivate));
- if (initializationData != null) {
- mimeType = MimeTypes.VIDEO_VC1;
- } else {
- Log.w(TAG, "Unsupported FourCC. Setting mimeType to " + MimeTypes.VIDEO_UNKNOWN);
- mimeType = MimeTypes.VIDEO_UNKNOWN;
- }
+ Pair> pair = parseFourCcPrivate(new ParsableByteArray(codecPrivate));
+ mimeType = pair.first;
+ initializationData = pair.second;
break;
case CODEC_ID_THEORA:
// TODO: This can be set to the real mimeType if/when we work out what initializationData
@@ -1931,39 +1921,44 @@ public final class MatroskaExtractor implements Extractor {
/**
* Builds initialization data for a {@link Format} from FourCC codec private data.
- *
- * VC1 is the only supported compression type.
*
- * @return The initialization data for the {@link Format}, or null if the compression type is
- * not VC1.
+ *
VC1 and H263 are the only supported compression types.
+ *
+ * @return The codec mime type and initialization data. If the compression type is not supported
+ * then the mime type is set to {@link MimeTypes#VIDEO_UNKNOWN} and the initialization data
+ * is {@code null}.
* @throws ParserException If the initialization data could not be built.
*/
- private static List parseFourCcVc1Private(ParsableByteArray buffer)
+ private static Pair> parseFourCcPrivate(ParsableByteArray buffer)
throws ParserException {
try {
buffer.skipBytes(16); // size(4), width(4), height(4), planes(2), bitcount(2).
long compression = buffer.readLittleEndianUnsignedInt();
- if (compression != FOURCC_COMPRESSION_VC1) {
- return null;
- }
-
- // Search for the initialization data from the end of the BITMAPINFOHEADER. The last 20
- // bytes of which are: sizeImage(4), xPel/m (4), yPel/m (4), clrUsed(4), clrImportant(4).
- int startOffset = buffer.getPosition() + 20;
- byte[] bufferData = buffer.data;
- for (int offset = startOffset; offset < bufferData.length - 4; offset++) {
- if (bufferData[offset] == 0x00 && bufferData[offset + 1] == 0x00
- && bufferData[offset + 2] == 0x01 && bufferData[offset + 3] == 0x0F) {
- // We've found the initialization data.
- byte[] initializationData = Arrays.copyOfRange(bufferData, offset, bufferData.length);
- return Collections.singletonList(initializationData);
+ if (compression == FOURCC_COMPRESSION_DIVX) {
+ return new Pair<>(MimeTypes.VIDEO_H263, null);
+ } else if (compression == FOURCC_COMPRESSION_VC1) {
+ // Search for the initialization data from the end of the BITMAPINFOHEADER. The last 20
+ // bytes of which are: sizeImage(4), xPel/m (4), yPel/m (4), clrUsed(4), clrImportant(4).
+ int startOffset = buffer.getPosition() + 20;
+ byte[] bufferData = buffer.data;
+ for (int offset = startOffset; offset < bufferData.length - 4; offset++) {
+ if (bufferData[offset] == 0x00
+ && bufferData[offset + 1] == 0x00
+ && bufferData[offset + 2] == 0x01
+ && bufferData[offset + 3] == 0x0F) {
+ // We've found the initialization data.
+ byte[] initializationData = Arrays.copyOfRange(bufferData, offset, bufferData.length);
+ return new Pair<>(MimeTypes.VIDEO_VC1, Collections.singletonList(initializationData));
+ }
}
+ throw new ParserException("Failed to find FourCC VC1 initialization data");
}
-
- throw new ParserException("Failed to find FourCC VC1 initialization data");
} catch (ArrayIndexOutOfBoundsException e) {
- throw new ParserException("Error parsing FourCC VC1 codec private");
+ throw new ParserException("Error parsing FourCC private data");
}
+
+ Log.w(TAG, "Unknown FourCC. Setting mimeType to " + MimeTypes.VIDEO_UNKNOWN);
+ return new Pair<>(MimeTypes.VIDEO_UNKNOWN, null);
}
/**
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java
index 5c56dc460a..bd786191a0 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java
@@ -24,6 +24,7 @@ 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.GaplessInfoHolder;
+import com.google.android.exoplayer2.extractor.Id3Peeker;
import com.google.android.exoplayer2.extractor.MpegAudioHeader;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap;
@@ -99,6 +100,7 @@ public final class Mp3Extractor implements Extractor {
private final ParsableByteArray scratch;
private final MpegAudioHeader synchronizedHeader;
private final GaplessInfoHolder gaplessInfoHolder;
+ private final Id3Peeker id3Peeker;
// Extractor outputs.
private ExtractorOutput extractorOutput;
@@ -135,6 +137,7 @@ public final class Mp3Extractor implements Extractor {
synchronizedHeader = new MpegAudioHeader();
gaplessInfoHolder = new GaplessInfoHolder();
basisTimeUs = C.TIME_UNSET;
+ id3Peeker = new Id3Peeker();
}
// Extractor implementation.
@@ -181,11 +184,23 @@ public final class Mp3Extractor implements Extractor {
seeker = getConstantBitrateSeeker(input);
}
extractorOutput.seekMap(seeker);
- trackOutput.format(Format.createAudioSampleFormat(null, synchronizedHeader.mimeType, null,
- Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, synchronizedHeader.channels,
- synchronizedHeader.sampleRate, Format.NO_VALUE, gaplessInfoHolder.encoderDelay,
- gaplessInfoHolder.encoderPadding, null, null, 0, null,
- (flags & FLAG_DISABLE_ID3_METADATA) != 0 ? null : metadata));
+ trackOutput.format(
+ Format.createAudioSampleFormat(
+ /* id= */ null,
+ synchronizedHeader.mimeType,
+ /* codecs= */ null,
+ /* bitrate= */ Format.NO_VALUE,
+ MpegAudioHeader.MAX_FRAME_SIZE_BYTES,
+ synchronizedHeader.channels,
+ synchronizedHeader.sampleRate,
+ /* pcmEncoding= */ Format.NO_VALUE,
+ gaplessInfoHolder.encoderDelay,
+ gaplessInfoHolder.encoderPadding,
+ /* initializationData= */ null,
+ /* drmInitData= */ null,
+ /* selectionFlags= */ 0,
+ /* language= */ null,
+ (flags & FLAG_DISABLE_ID3_METADATA) != 0 ? null : metadata));
}
return readSample(input);
}
@@ -242,7 +257,15 @@ public final class Mp3Extractor implements Extractor {
int searchLimitBytes = sniffing ? MAX_SNIFF_BYTES : MAX_SYNC_BYTES;
input.resetPeekPosition();
if (input.getPosition() == 0) {
- peekId3Data(input);
+ // We need to parse enough ID3 metadata to retrieve any gapless playback information even
+ // if ID3 metadata parsing is disabled.
+ boolean onlyDecodeGaplessInfoFrames = (flags & FLAG_DISABLE_ID3_METADATA) != 0;
+ Id3Decoder.FramePredicate id3FramePredicate =
+ onlyDecodeGaplessInfoFrames ? GaplessInfoHolder.GAPLESS_INFO_ID3_FRAME_PREDICATE : null;
+ metadata = id3Peeker.peekId3Data(input, id3FramePredicate);
+ if (metadata != null) {
+ gaplessInfoHolder.setFromMetadata(metadata);
+ }
peekedId3Bytes = (int) input.getPeekPosition();
if (!sniffing) {
input.skipFully(peekedId3Bytes);
@@ -296,49 +319,6 @@ public final class Mp3Extractor implements Extractor {
return true;
}
- /**
- * Peeks ID3 data from the input, including gapless playback information.
- *
- * @param input The {@link ExtractorInput} from which data should be peeked.
- * @throws IOException If an error occurred peeking from the input.
- * @throws InterruptedException If the thread was interrupted.
- */
- private void peekId3Data(ExtractorInput input) throws IOException, InterruptedException {
- int peekedId3Bytes = 0;
- while (true) {
- input.peekFully(scratch.data, 0, Id3Decoder.ID3_HEADER_LENGTH);
- scratch.setPosition(0);
- if (scratch.readUnsignedInt24() != Id3Decoder.ID3_TAG) {
- // Not an ID3 tag.
- break;
- }
- scratch.skipBytes(3); // Skip major version, minor version and flags.
- int framesLength = scratch.readSynchSafeInt();
- int tagLength = Id3Decoder.ID3_HEADER_LENGTH + framesLength;
-
- if (metadata == null) {
- byte[] id3Data = new byte[tagLength];
- System.arraycopy(scratch.data, 0, id3Data, 0, Id3Decoder.ID3_HEADER_LENGTH);
- input.peekFully(id3Data, Id3Decoder.ID3_HEADER_LENGTH, framesLength);
- // We need to parse enough ID3 metadata to retrieve any gapless playback information even
- // if ID3 metadata parsing is disabled.
- Id3Decoder.FramePredicate id3FramePredicate = (flags & FLAG_DISABLE_ID3_METADATA) != 0
- ? GaplessInfoHolder.GAPLESS_INFO_ID3_FRAME_PREDICATE : null;
- metadata = new Id3Decoder(id3FramePredicate).decode(id3Data, tagLength);
- if (metadata != null) {
- gaplessInfoHolder.setFromMetadata(metadata);
- }
- } else {
- input.advancePeekPosition(framesLength);
- }
-
- peekedId3Bytes += tagLength;
- }
-
- input.resetPeekPosition();
- input.advancePeekPosition(peekedId3Bytes);
- }
-
/**
* Consumes the next frame from the {@code input} if it contains VBRI or Xing seeking metadata,
* returning a {@link Seeker} if the metadata was present and valid, or {@code null} otherwise.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java
index 30358ff7c7..a6e2524f0b 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java
@@ -53,6 +53,12 @@ import java.util.List;
private static final int TYPE_clcp = Util.getIntegerCodeForString("clcp");
private static final int TYPE_meta = Util.getIntegerCodeForString("meta");
+ /**
+ * The threshold number of samples to trim from the start/end of an audio track when applying an
+ * edit below which gapless info can be used (rather than removing samples from the sample table).
+ */
+ private static final int MAX_GAPLESS_TRIM_SIZE_SAMPLES = 3;
+
/**
* Parses a trak atom (defined in 14496-12).
*
@@ -311,22 +317,18 @@ import java.util.List;
// See the BMFF spec (ISO 14496-12) subsection 8.6.6. Edit lists that require prerolling from a
// sync sample after reordering are not supported. Partial audio sample truncation is only
- // supported in edit lists with one edit that removes less than one sample from the start/end of
- // the track, for gapless audio playback. This implementation handles simple discarding/delaying
- // of samples. The extractor may place further restrictions on what edited streams are playable.
+ // supported in edit lists with one edit that removes less than MAX_GAPLESS_TRIM_SIZE_SAMPLES
+ // samples from the start/end of the track. This implementation handles simple
+ // discarding/delaying of samples. The extractor may place further restrictions on what edited
+ // streams are playable.
- if (track.editListDurations.length == 1 && track.type == C.TRACK_TYPE_AUDIO
+ if (track.editListDurations.length == 1
+ && track.type == C.TRACK_TYPE_AUDIO
&& timestamps.length >= 2) {
- // Handle the edit by setting gapless playback metadata, if possible. This implementation
- // assumes that only one "roll" sample is needed, which is the case for AAC, so the start/end
- // points of the edit must lie within the first/last samples respectively.
long editStartTime = track.editListMediaTimes[0];
long editEndTime = editStartTime + Util.scaleLargeTimestamp(track.editListDurations[0],
track.timescale, track.movieTimescale);
- if (timestamps[0] <= editStartTime
- && editStartTime < timestamps[1]
- && timestamps[timestamps.length - 1] < editEndTime
- && editEndTime <= duration) {
+ if (canApplyEditWithGaplessInfo(timestamps, duration, editStartTime, editEndTime)) {
long paddingTimeUnits = duration - editEndTime;
long encoderDelay = Util.scaleLargeTimestamp(editStartTime - timestamps[0],
track.format.sampleRate, track.timescale);
@@ -1180,6 +1182,19 @@ import java.util.List;
return size;
}
+ /** Returns whether it's possible to apply the specified edit using gapless playback info. */
+ private static boolean canApplyEditWithGaplessInfo(
+ long[] timestamps, long duration, long editStartTime, long editEndTime) {
+ int lastIndex = timestamps.length - 1;
+ int latestDelayIndex = Util.constrainValue(MAX_GAPLESS_TRIM_SIZE_SAMPLES, 0, lastIndex);
+ int earliestPaddingIndex =
+ Util.constrainValue(timestamps.length - MAX_GAPLESS_TRIM_SIZE_SAMPLES, 0, lastIndex);
+ return timestamps[0] <= editStartTime
+ && editStartTime < timestamps[latestDelayIndex]
+ && timestamps[earliestPaddingIndex] < editEndTime
+ && editEndTime <= duration;
+ }
+
private AtomParsers() {
// Prevent instantiation.
}
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 7e40f6d2ee..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;
@@ -134,11 +134,9 @@ public final class FragmentedMp4Extractor implements Extractor {
private final ParsableByteArray nalStartCode;
private final ParsableByteArray nalPrefix;
private final ParsableByteArray nalBuffer;
- private final ParsableByteArray encryptionSignalByte;
- private final ParsableByteArray defaultInitializationVector;
// Adjusts sample timestamps.
- private final TimestampAdjuster timestampAdjuster;
+ private final @Nullable TimestampAdjuster timestampAdjuster;
// Parser state.
private final ParsableByteArray atomHeader;
@@ -154,6 +152,7 @@ public final class FragmentedMp4Extractor implements Extractor {
private ParsableByteArray atomData;
private long endOfMdatPosition;
private int pendingMetadataSampleBytes;
+ private long pendingSeekTimeUs;
private long durationUs;
private long segmentIndexEarliestPresentationTimeUs;
@@ -186,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());
}
@@ -207,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);
}
@@ -223,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
@@ -233,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