downloads) {
+ return notificationHelper.buildProgressNotification(
+ R.drawable.ic_download, /* contentIntent= */ null, /* message= */ null, downloads);
}
@Override
- protected void onTaskStateChanged(TaskState taskState) {
- if (taskState.action.isRemoveAction) {
+ protected void onDownloadChanged(Download download) {
+ Notification notification;
+ if (download.state == Download.STATE_COMPLETED) {
+ notification =
+ notificationHelper.buildDownloadCompletedNotification(
+ R.drawable.ic_download_done,
+ /* contentIntent= */ null,
+ Util.fromUtf8Bytes(download.request.data));
+ } else if (download.state == Download.STATE_FAILED) {
+ notification =
+ notificationHelper.buildDownloadFailedNotification(
+ R.drawable.ic_download_done,
+ /* contentIntent= */ null,
+ Util.fromUtf8Bytes(download.request.data));
+ } else {
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);
+ NotificationUtil.setNotification(this, nextNotificationId++, notification);
}
}
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
index be2dec71d5..839ed304bd 100644
--- 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
@@ -15,54 +15,32 @@
*/
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 androidx.annotation.Nullable;
+import androidx.fragment.app.FragmentManager;
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.RenderersFactory;
+import com.google.android.exoplayer2.offline.Download;
+import com.google.android.exoplayer2.offline.DownloadCursor;
import com.google.android.exoplayer2.offline.DownloadHelper;
+import com.google.android.exoplayer2.offline.DownloadIndex;
import com.google.android.exoplayer2.offline.DownloadManager;
-import com.google.android.exoplayer2.offline.DownloadManager.TaskState;
+import com.google.android.exoplayer2.offline.DownloadRequest;
import com.google.android.exoplayer2.offline.DownloadService;
-import com.google.android.exoplayer2.offline.ProgressiveDownloadHelper;
-import com.google.android.exoplayer2.offline.StreamKey;
-import com.google.android.exoplayer2.offline.TrackKey;
-import com.google.android.exoplayer2.source.TrackGroup;
-import com.google.android.exoplayer2.source.TrackGroupArray;
-import com.google.android.exoplayer2.source.dash.offline.DashDownloadHelper;
-import com.google.android.exoplayer2.source.hls.offline.HlsDownloadHelper;
-import com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloadHelper;
-import com.google.android.exoplayer2.ui.DefaultTrackNameProvider;
-import com.google.android.exoplayer2.ui.TrackNameProvider;
+import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
+import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
-import java.io.File;
import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
import java.util.HashMap;
-import java.util.List;
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 {
+/** Tracks media that has been downloaded. */
+public class DownloadTracker {
/** Listens for changes in the tracked downloads. */
public interface Listener {
@@ -75,28 +53,23 @@ public class DownloadTracker implements DownloadManager.Listener {
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;
+ private final HashMap downloads;
+ private final DownloadIndex downloadIndex;
+ private final DefaultTrackSelector.Parameters trackSelectorParameters;
+
+ @Nullable private StartDownloadDialogHelper startDownloadDialogHelper;
public DownloadTracker(
- Context context,
- DataSource.Factory dataSourceFactory,
- File actionFile,
- DownloadAction.Deserializer... deserializers) {
+ Context context, DataSource.Factory dataSourceFactory, DownloadManager downloadManager) {
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.length > 0 ? deserializers : DownloadAction.getDefaultDeserializers());
+ downloads = new HashMap<>();
+ downloadIndex = downloadManager.getDownloadIndex();
+ trackSelectorParameters = DownloadHelper.getDefaultTrackSelectorParameters(context);
+ downloadManager.addListener(new DownloadManagerListener());
+ loadDownloads();
}
public void addListener(Listener listener) {
@@ -108,191 +81,189 @@ public class DownloadTracker implements DownloadManager.Listener {
}
public boolean isDownloaded(Uri uri) {
- return trackedDownloadStates.containsKey(uri);
+ Download download = downloads.get(uri);
+ return download != null && download.state != Download.STATE_FAILED;
}
- @SuppressWarnings("unchecked")
- public List getOfflineStreamKeys(Uri uri) {
- if (!trackedDownloadStates.containsKey(uri)) {
- return Collections.emptyList();
- }
- return trackedDownloadStates.get(uri).getKeys();
+ public DownloadRequest getDownloadRequest(Uri uri) {
+ Download download = downloads.get(uri);
+ return download != null && download.state != Download.STATE_FAILED ? download.request : null;
}
- 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);
+ public void toggleDownload(
+ FragmentManager fragmentManager,
+ String name,
+ Uri uri,
+ String extension,
+ RenderersFactory renderersFactory) {
+ Download download = downloads.get(uri);
+ if (download != null) {
+ DownloadService.sendRemoveDownload(
+ context, DemoDownloadService.class, download.request.id, /* foreground= */ false);
} 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();
+ if (startDownloadDialogHelper != null) {
+ startDownloadDialogHelper.release();
}
+ startDownloadDialogHelper =
+ new StartDownloadDialogHelper(
+ fragmentManager, getDownloadHelper(uri, extension, renderersFactory), name);
}
}
- @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);
+ private void loadDownloads() {
+ try (DownloadCursor loadedDownloads = downloadIndex.getDownloads()) {
+ while (loadedDownloads.moveToNext()) {
+ Download download = loadedDownloads.getDownload();
+ downloads.put(download.request.uri, download);
}
} catch (IOException e) {
- Log.e(TAG, "Failed to load tracked actions", e);
+ Log.w(TAG, "Failed to query downloads", e);
}
}
- private void handleTrackedDownloadStatesChanged() {
- for (Listener listener : listeners) {
- listener.onDownloadsChanged();
- }
- final DownloadAction[] actions = trackedDownloadStates.values().toArray(new DownloadAction[0]);
- actionFileWriteHandler.post(
- () -> {
- 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) {
+ private DownloadHelper getDownloadHelper(
+ Uri uri, String extension, RenderersFactory renderersFactory) {
int type = Util.inferContentType(uri, extension);
switch (type) {
case C.TYPE_DASH:
- return new DashDownloadHelper(uri, dataSourceFactory);
+ return DownloadHelper.forDash(context, uri, dataSourceFactory, renderersFactory);
case C.TYPE_SS:
- return new SsDownloadHelper(uri, dataSourceFactory);
+ return DownloadHelper.forSmoothStreaming(context, uri, dataSourceFactory, renderersFactory);
case C.TYPE_HLS:
- return new HlsDownloadHelper(uri, dataSourceFactory);
+ return DownloadHelper.forHls(context, uri, dataSourceFactory, renderersFactory);
case C.TYPE_OTHER:
- return new ProgressiveDownloadHelper(uri);
+ return DownloadHelper.forProgressive(context, uri);
default:
throw new IllegalStateException("Unsupported type: " + type);
}
}
- private final class StartDownloadDialogHelper
- implements DownloadHelper.Callback, DialogInterface.OnClickListener {
+ private class DownloadManagerListener implements DownloadManager.Listener {
- 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 onDownloadChanged(DownloadManager downloadManager, Download download) {
+ downloads.put(download.request.uri, download);
+ for (Listener listener : listeners) {
+ listener.onDownloadsChanged();
+ }
}
+ @Override
+ public void onDownloadRemoved(DownloadManager downloadManager, Download download) {
+ downloads.remove(download.request.uri);
+ for (Listener listener : listeners) {
+ listener.onDownloadsChanged();
+ }
+ }
+ }
+
+ private final class StartDownloadDialogHelper
+ implements DownloadHelper.Callback,
+ DialogInterface.OnClickListener,
+ DialogInterface.OnDismissListener {
+
+ private final FragmentManager fragmentManager;
+ private final DownloadHelper downloadHelper;
+ private final String name;
+
+ private TrackSelectionDialog trackSelectionDialog;
+ private MappedTrackInfo mappedTrackInfo;
+
+ public StartDownloadDialogHelper(
+ FragmentManager fragmentManager, DownloadHelper downloadHelper, String name) {
+ this.fragmentManager = fragmentManager;
+ this.downloadHelper = downloadHelper;
+ this.name = name;
+ downloadHelper.prepare(this);
+ }
+
+ public void release() {
+ downloadHelper.release();
+ if (trackSelectionDialog != null) {
+ trackSelectionDialog.dismiss();
+ }
+ }
+
+ // DownloadHelper.Callback implementation.
+
@Override
public void onPrepared(DownloadHelper helper) {
- for (int i = 0; i < downloadHelper.getPeriodCount(); i++) {
- TrackGroupArray trackGroups = downloadHelper.getTrackGroups(i);
- for (int j = 0; j < trackGroups.length; j++) {
- TrackGroup trackGroup = trackGroups.get(j);
- for (int k = 0; k < trackGroup.length; k++) {
- trackKeys.add(new TrackKey(i, j, k));
- trackTitles.add(trackNameProvider.getTrackName(trackGroup.getFormat(k)));
- }
- }
+ if (helper.getPeriodCount() == 0) {
+ Log.d(TAG, "No periods found. Downloading entire stream.");
+ startDownload();
+ downloadHelper.release();
+ return;
}
- if (!trackKeys.isEmpty()) {
- builder.setView(dialogView);
+ mappedTrackInfo = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0);
+ if (!TrackSelectionDialog.willHaveContent(mappedTrackInfo)) {
+ Log.d(TAG, "No dialog content. Downloading entire stream.");
+ startDownload();
+ downloadHelper.release();
+ return;
}
- builder.create().show();
+ trackSelectionDialog =
+ TrackSelectionDialog.createForMappedTrackInfoAndParameters(
+ /* titleId= */ R.string.exo_download_description,
+ mappedTrackInfo,
+ trackSelectorParameters,
+ /* allowAdaptiveSelections =*/ false,
+ /* allowMultipleOverrides= */ true,
+ /* onClickListener= */ this,
+ /* onDismissListener= */ this);
+ trackSelectionDialog.show(fragmentManager, /* tag= */ null);
}
@Override
public void onPrepareError(DownloadHelper helper, IOException e) {
- Toast.makeText(
- context.getApplicationContext(), R.string.download_start_error, Toast.LENGTH_LONG)
- .show();
+ Toast.makeText(context, R.string.download_start_error, Toast.LENGTH_LONG).show();
Log.e(TAG, "Failed to start download", e);
}
+ // DialogInterface.OnClickListener implementation.
+
@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));
+ for (int periodIndex = 0; periodIndex < downloadHelper.getPeriodCount(); periodIndex++) {
+ downloadHelper.clearTrackSelections(periodIndex);
+ for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) {
+ if (!trackSelectionDialog.getIsDisabled(/* rendererIndex= */ i)) {
+ downloadHelper.addTrackSelectionForSingleRenderer(
+ periodIndex,
+ /* rendererIndex= */ i,
+ trackSelectorParameters,
+ trackSelectionDialog.getOverrides(/* rendererIndex= */ 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);
+ DownloadRequest downloadRequest = buildDownloadRequest();
+ if (downloadRequest.streamKeys.isEmpty()) {
+ // All tracks were deselected in the dialog. Don't start the download.
+ return;
}
+ startDownload(downloadRequest);
+ }
+
+ // DialogInterface.OnDismissListener implementation.
+
+ @Override
+ public void onDismiss(DialogInterface dialogInterface) {
+ trackSelectionDialog = null;
+ downloadHelper.release();
+ }
+
+ // Internal methods.
+
+ private void startDownload() {
+ startDownload(buildDownloadRequest());
+ }
+
+ private void startDownload(DownloadRequest downloadRequest) {
+ DownloadService.sendAddDownload(
+ context, DemoDownloadService.class, downloadRequest, /* foreground= */ false);
+ }
+
+ private DownloadRequest buildDownloadRequest() {
+ return downloadHelper.getDownloadRequest(Util.getUtf8Bytes(name));
}
}
}
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 ffa9bafa4f..1e231dd45e 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
@@ -15,54 +15,51 @@
*/
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.support.annotation.NonNull;
-import android.support.annotation.Nullable;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
import android.util.Pair;
import android.view.KeyEvent;
import android.view.View;
import android.view.View.OnClickListener;
-import android.view.ViewGroup;
import android.widget.Button;
-import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.C.ContentType;
-import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.PlaybackPreparer;
import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.SimpleExoPlayer;
+import com.google.android.exoplayer2.demo.Sample.UriSample;
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.offline.StreamKey;
+import com.google.android.exoplayer2.offline.DownloadHelper;
+import com.google.android.exoplayer2.offline.DownloadRequest;
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.MediaSourceFactory;
+import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.ads.AdsLoader;
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
import com.google.android.exoplayer2.source.dash.DashMediaSource;
-import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser;
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
-import com.google.android.exoplayer2.source.hls.playlist.DefaultHlsPlaylistParserFactory;
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
-import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser;
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
@@ -72,7 +69,6 @@ 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.ui.spherical.SphericalSurfaceView;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource;
@@ -83,42 +79,48 @@ import java.lang.reflect.Constructor;
import java.net.CookieHandler;
import java.net.CookieManager;
import java.net.CookiePolicy;
-import java.util.List;
+import java.util.ArrayList;
import java.util.UUID;
/** An activity that plays media using {@link SimpleExoPlayer}. */
-public class PlayerActivity extends Activity
+public class PlayerActivity extends AppCompatActivity
implements OnClickListener, PlaybackPreparer, PlayerControlView.VisibilityListener {
- public static final String DRM_SCHEME_EXTRA = "drm_scheme";
- 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";
-
- public static final String ACTION_VIEW_LIST =
- "com.google.android.exoplayer.demo.action.VIEW_LIST";
- public static final String URI_LIST_EXTRA = "uri_list";
- public static final String EXTENSION_LIST_EXTRA = "extension_list";
-
- public static final String AD_TAG_URI_EXTRA = "ad_tag_uri";
-
- public static final String ABR_ALGORITHM_EXTRA = "abr_algorithm";
- public static final String ABR_ALGORITHM_DEFAULT = "default";
- public static final String ABR_ALGORITHM_RANDOM = "random";
+ // Activity extras.
public static final String SPHERICAL_STEREO_MODE_EXTRA = "spherical_stereo_mode";
public static final String SPHERICAL_STEREO_MODE_MONO = "mono";
public static final String SPHERICAL_STEREO_MODE_TOP_BOTTOM = "top_bottom";
public static final String SPHERICAL_STEREO_MODE_LEFT_RIGHT = "left_right";
+ // Actions.
+
+ public static final String ACTION_VIEW = "com.google.android.exoplayer.demo.action.VIEW";
+ public static final String ACTION_VIEW_LIST =
+ "com.google.android.exoplayer.demo.action.VIEW_LIST";
+
+ // Player configuration extras.
+
+ public static final String ABR_ALGORITHM_EXTRA = "abr_algorithm";
+ public static final String ABR_ALGORITHM_DEFAULT = "default";
+ public static final String ABR_ALGORITHM_RANDOM = "random";
+
+ // Media item configuration extras.
+
+ public static final String URI_EXTRA = "uri";
+ public static final String EXTENSION_EXTRA = "extension";
+
+ public static final String DRM_SCHEME_EXTRA = "drm_scheme";
+ 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 AD_TAG_URI_EXTRA = "ad_tag_uri";
// For backwards compatibility only.
- private static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid";
+ public 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";
@@ -130,13 +132,16 @@ public class PlayerActivity extends Activity
DEFAULT_COOKIE_MANAGER.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
}
+ private final ArrayList mediaDrms;
+
private PlayerView playerView;
private LinearLayout debugRootView;
+ private Button selectTracksButton;
private TextView debugTextView;
+ private boolean isShowingTrackSelectionDialog;
private DataSource.Factory dataSourceFactory;
private SimpleExoPlayer player;
- private FrameworkMediaDrm mediaDrm;
private MediaSource mediaSource;
private DefaultTrackSelector trackSelector;
private DefaultTrackSelector.Parameters trackSelectorParameters;
@@ -151,13 +156,17 @@ public class PlayerActivity extends Activity
private AdsLoader adsLoader;
private Uri loadedAdTagUri;
- private ViewGroup adUiViewGroup;
+
+ public PlayerActivity() {
+ mediaDrms = new ArrayList<>();
+ }
// Activity lifecycle
@Override
public void onCreate(Bundle savedInstanceState) {
- String sphericalStereoMode = getIntent().getStringExtra(SPHERICAL_STEREO_MODE_EXTRA);
+ Intent intent = getIntent();
+ String sphericalStereoMode = intent.getStringExtra(SPHERICAL_STEREO_MODE_EXTRA);
if (sphericalStereoMode != null) {
setTheme(R.style.PlayerTheme_Spherical);
}
@@ -168,10 +177,10 @@ public class PlayerActivity extends Activity
}
setContentView(R.layout.player_activity);
- View rootView = findViewById(R.id.root);
- rootView.setOnClickListener(this);
debugRootView = findViewById(R.id.controls_root);
debugTextView = findViewById(R.id.debug_text_view);
+ selectTracksButton = findViewById(R.id.select_tracks_button);
+ selectTracksButton.setOnClickListener(this);
playerView = findViewById(R.id.player_view);
playerView.setControllerVisibilityListener(this);
@@ -199,13 +208,14 @@ public class PlayerActivity extends Activity
startWindow = savedInstanceState.getInt(KEY_WINDOW);
startPosition = savedInstanceState.getLong(KEY_POSITION);
} else {
- trackSelectorParameters = new DefaultTrackSelector.ParametersBuilder().build();
+ trackSelectorParameters = DefaultTrackSelector.Parameters.getDefaults(/* context= */ this);
clearStartPosition();
}
}
@Override
public void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
releasePlayer();
releaseAdsLoader();
clearStartPosition();
@@ -280,6 +290,7 @@ public class PlayerActivity extends Activity
@Override
public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
updateTrackSelectorParameters();
updateStartPosition();
outState.putParcelable(KEY_TRACK_SELECTOR_PARAMETERS, trackSelectorParameters);
@@ -300,23 +311,15 @@ public class PlayerActivity extends Activity
@Override
public void onClick(View view) {
- if (view.getParent() == debugRootView) {
- MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
- if (mappedTrackInfo != null) {
- 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();
- }
+ if (view == selectTracksButton
+ && !isShowingTrackSelectionDialog
+ && TrackSelectionDialog.willHaveContent(trackSelector)) {
+ isShowingTrackSelectionDialog = true;
+ TrackSelectionDialog trackSelectionDialog =
+ TrackSelectionDialog.createForTrackSelector(
+ trackSelector,
+ /* onDismissListener= */ dismissedDialog -> isShowingTrackSelectionDialog = false);
+ trackSelectionDialog.show(getSupportFragmentManager(), /* tag= */ null);
}
}
@@ -324,7 +327,7 @@ public class PlayerActivity extends Activity
@Override
public void preparePlayback() {
- initializePlayer();
+ player.retry();
}
// PlaybackControlView.VisibilityListener implementation
@@ -339,67 +342,11 @@ public class PlayerActivity extends Activity
private void initializePlayer() {
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.checkCleartextTrafficPermitted(uris)) {
- showToast(R.string.error_cleartext_not_permitted);
- return;
- }
- if (Util.maybeRequestReadExternalStoragePermission(/* activity= */ this, uris)) {
- // The player will be reinitialized if the permission is granted.
- return;
- }
- DefaultDrmSessionManager drmSessionManager = null;
- if (intent.hasExtra(DRM_SCHEME_EXTRA) || intent.hasExtra(DRM_SCHEME_UUID_EXTRA)) {
- 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;
- } else {
- try {
- String drmSchemeExtra = intent.hasExtra(DRM_SCHEME_EXTRA) ? DRM_SCHEME_EXTRA
- : DRM_SCHEME_UUID_EXTRA;
- UUID drmSchemeUuid = Util.getDrmUuid(intent.getStringExtra(drmSchemeExtra));
- if (drmSchemeUuid == null) {
- errorStringId = R.string.error_drm_unsupported_scheme;
- } else {
- drmSessionManager =
- buildDrmSessionManagerV18(
- drmSchemeUuid, drmLicenseUrl, keyRequestPropertiesArray, multiSession);
- }
- } catch (UnsupportedDrmException e) {
- errorStringId = e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME
- ? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown;
- }
- }
- if (drmSessionManager == null) {
- showToast(errorStringId);
- finish();
- return;
- }
+ releaseMediaDrms();
+ mediaSource = createTopLevelMediaSource(intent);
+ if (mediaSource == null) {
+ return;
}
TrackSelection.Factory trackSelectionFactory;
@@ -416,21 +363,15 @@ public class PlayerActivity extends Activity
boolean preferExtensionDecoders =
intent.getBooleanExtra(PREFER_EXTENSION_DECODERS_EXTRA, false);
- @DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode =
- ((DemoApplication) getApplication()).useExtensionRenderers()
- ? (preferExtensionDecoders ? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
- : DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
- : DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF;
- DefaultRenderersFactory renderersFactory =
- new DefaultRenderersFactory(this, extensionRendererMode);
+ RenderersFactory renderersFactory =
+ ((DemoApplication) getApplication()).buildRenderersFactory(preferExtensionDecoders);
- trackSelector = new DefaultTrackSelector(trackSelectionFactory);
+ trackSelector = new DefaultTrackSelector(/* context= */ this, trackSelectionFactory);
trackSelector.setParameters(trackSelectorParameters);
lastSeenTrackGroupArray = null;
player =
- ExoPlayerFactory.newSimpleInstance(
- /* context= */ this, renderersFactory, trackSelector, drmSessionManager);
+ ExoPlayerFactory.newSimpleInstance(/* context= */ this, renderersFactory, trackSelector);
player.addListener(new PlayerEventListener());
player.setPlayWhenReady(startAutoPlay);
player.addAnalyticsListener(new EventLogger(trackSelector));
@@ -438,28 +379,8 @@ public class PlayerActivity extends Activity
playerView.setPlaybackPreparer(this);
debugViewHelper = new DebugTextViewHelper(player, debugTextView);
debugViewHelper.start();
-
- MediaSource[] mediaSources = new MediaSource[uris.length];
- for (int i = 0; i < uris.length; i++) {
- mediaSources[i] = buildMediaSource(uris[i], extensions[i]);
- }
- 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 {
- releaseAdsLoader();
+ if (adsLoader != null) {
+ adsLoader.setPlayer(player);
}
}
boolean haveStartPosition = startWindow != C.INDEX_UNSET;
@@ -467,44 +388,138 @@ public class PlayerActivity extends Activity
player.seekTo(startWindow, startPosition);
}
player.prepare(mediaSource, !haveStartPosition, false);
- updateButtonVisibilities();
+ updateButtonVisibility();
}
- private MediaSource buildMediaSource(Uri uri) {
- return buildMediaSource(uri, null);
+ @Nullable
+ private MediaSource createTopLevelMediaSource(Intent intent) {
+ String action = intent.getAction();
+ boolean actionIsListView = ACTION_VIEW_LIST.equals(action);
+ if (!actionIsListView && !ACTION_VIEW.equals(action)) {
+ showToast(getString(R.string.unexpected_intent_action, action));
+ finish();
+ return null;
+ }
+
+ Sample intentAsSample = Sample.createFromIntent(intent);
+ UriSample[] samples =
+ intentAsSample instanceof Sample.PlaylistSample
+ ? ((Sample.PlaylistSample) intentAsSample).children
+ : new UriSample[] {(UriSample) intentAsSample};
+
+ boolean seenAdsTagUri = false;
+ for (UriSample sample : samples) {
+ seenAdsTagUri |= sample.adTagUri != null;
+ if (!Util.checkCleartextTrafficPermitted(sample.uri)) {
+ showToast(R.string.error_cleartext_not_permitted);
+ return null;
+ }
+ if (Util.maybeRequestReadExternalStoragePermission(/* activity= */ this, sample.uri)) {
+ // The player will be reinitialized if the permission is granted.
+ return null;
+ }
+ }
+
+ MediaSource[] mediaSources = new MediaSource[samples.length];
+ for (int i = 0; i < samples.length; i++) {
+ mediaSources[i] = createLeafMediaSource(samples[i]);
+ }
+ MediaSource mediaSource =
+ mediaSources.length == 1 ? mediaSources[0] : new ConcatenatingMediaSource(mediaSources);
+
+ if (seenAdsTagUri) {
+ Uri adTagUri = samples[0].adTagUri;
+ if (actionIsListView) {
+ showToast(R.string.unsupported_ads_in_concatenation);
+ } else {
+ if (!adTagUri.equals(loadedAdTagUri)) {
+ releaseAdsLoader();
+ loadedAdTagUri = adTagUri;
+ }
+ MediaSource adsMediaSource = createAdsMediaSource(mediaSource, adTagUri);
+ if (adsMediaSource != null) {
+ mediaSource = adsMediaSource;
+ } else {
+ showToast(R.string.ima_not_loaded);
+ }
+ }
+ } else {
+ releaseAdsLoader();
+ }
+
+ return mediaSource;
}
- @SuppressWarnings("unchecked")
- private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) {
- @ContentType int type = Util.inferContentType(uri, overrideExtension);
+ private MediaSource createLeafMediaSource(UriSample parameters) {
+ DrmSessionManager drmSessionManager = null;
+ Sample.DrmInfo drmInfo = parameters.drmInfo;
+ if (drmInfo != null) {
+ int errorStringId = R.string.error_drm_unknown;
+ if (Util.SDK_INT < 18) {
+ errorStringId = R.string.error_drm_not_supported;
+ } else {
+ try {
+ if (drmInfo.drmScheme == null) {
+ errorStringId = R.string.error_drm_unsupported_scheme;
+ } else {
+ drmSessionManager =
+ buildDrmSessionManagerV18(
+ drmInfo.drmScheme,
+ drmInfo.drmLicenseUrl,
+ drmInfo.drmKeyRequestProperties,
+ drmInfo.drmMultiSession);
+ }
+ } catch (UnsupportedDrmException e) {
+ errorStringId =
+ e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME
+ ? R.string.error_drm_unsupported_scheme
+ : R.string.error_drm_unknown;
+ }
+ }
+ if (drmSessionManager == null) {
+ showToast(errorStringId);
+ finish();
+ return null;
+ }
+ } else {
+ drmSessionManager = DrmSessionManager.getDummyDrmSessionManager();
+ }
+
+ DownloadRequest downloadRequest =
+ ((DemoApplication) getApplication())
+ .getDownloadTracker()
+ .getDownloadRequest(parameters.uri);
+ if (downloadRequest != null) {
+ return DownloadHelper.createMediaSource(downloadRequest, dataSourceFactory);
+ }
+ return createLeafMediaSource(parameters.uri, parameters.extension, drmSessionManager);
+ }
+
+ private MediaSource createLeafMediaSource(
+ Uri uri, String extension, DrmSessionManager drmSessionManager) {
+ @ContentType int type = Util.inferContentType(uri, extension);
switch (type) {
case C.TYPE_DASH:
return new DashMediaSource.Factory(dataSourceFactory)
- .setManifestParser(
- new FilteringManifestParser<>(new DashManifestParser(), getOfflineStreamKeys(uri)))
+ .setDrmSessionManager(drmSessionManager)
.createMediaSource(uri);
case C.TYPE_SS:
return new SsMediaSource.Factory(dataSourceFactory)
- .setManifestParser(
- new FilteringManifestParser<>(new SsManifestParser(), getOfflineStreamKeys(uri)))
+ .setDrmSessionManager(drmSessionManager)
.createMediaSource(uri);
case C.TYPE_HLS:
return new HlsMediaSource.Factory(dataSourceFactory)
- .setPlaylistParserFactory(
- new DefaultHlsPlaylistParserFactory(getOfflineStreamKeys(uri)))
+ .setDrmSessionManager(drmSessionManager)
.createMediaSource(uri);
case C.TYPE_OTHER:
- return new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
- default: {
+ return new ProgressiveMediaSource.Factory(dataSourceFactory)
+ .setDrmSessionManager(drmSessionManager)
+ .createMediaSource(uri);
+ default:
throw new IllegalStateException("Unsupported type: " + type);
- }
}
}
- private List getOfflineStreamKeys(Uri uri) {
- return ((DemoApplication) getApplication()).getDownloadTracker().getOfflineStreamKeys(uri);
- }
-
private DefaultDrmSessionManager buildDrmSessionManagerV18(
UUID uuid, String licenseUrl, String[] keyRequestPropertiesArray, boolean multiSession)
throws UnsupportedDrmException {
@@ -518,8 +533,9 @@ public class PlayerActivity extends Activity
keyRequestPropertiesArray[i + 1]);
}
}
- releaseMediaDrm();
- mediaDrm = FrameworkMediaDrm.newInstance(uuid);
+
+ FrameworkMediaDrm mediaDrm = FrameworkMediaDrm.newInstance(uuid);
+ mediaDrms.add(mediaDrm);
return new DefaultDrmSessionManager<>(uuid, mediaDrm, drmCallback, null, multiSession);
}
@@ -534,14 +550,17 @@ public class PlayerActivity extends Activity
mediaSource = null;
trackSelector = null;
}
- releaseMediaDrm();
+ if (adsLoader != null) {
+ adsLoader.setPlayer(null);
+ }
+ releaseMediaDrms();
}
- private void releaseMediaDrm() {
- if (mediaDrm != null) {
+ private void releaseMediaDrms() {
+ for (FrameworkMediaDrm mediaDrm : mediaDrms) {
mediaDrm.release();
- mediaDrm = null;
}
+ mediaDrms.clear();
}
private void releaseAdsLoader() {
@@ -579,7 +598,8 @@ public class PlayerActivity extends Activity
}
/** Returns an ads media source, reusing the ads loader if one exists. */
- private @Nullable MediaSource createAdsMediaSource(MediaSource mediaSource, Uri adTagUri) {
+ @Nullable
+ private MediaSource createAdsMediaSource(MediaSource mediaSource, Uri adTagUri) {
// Load the extension source using reflection so the demo app doesn't have to depend on it.
// The ads loader is reused for multiple playbacks, so that ad playback can resume.
try {
@@ -593,15 +613,13 @@ public class PlayerActivity extends Activity
.getConstructor(android.content.Context.class, android.net.Uri.class);
// LINT.ThenChange(../../../../../../../../proguard-rules.txt)
adsLoader = loaderConstructor.newInstance(this, adTagUri);
- adUiViewGroup = new FrameLayout(this);
- // The demo app has a non-null overlay frame layout.
- playerView.getOverlayFrameLayout().addView(adUiViewGroup);
}
- AdsMediaSource.MediaSourceFactory adMediaSourceFactory =
- new AdsMediaSource.MediaSourceFactory() {
+ MediaSourceFactory adMediaSourceFactory =
+ new MediaSourceFactory() {
@Override
public MediaSource createMediaSource(Uri uri) {
- return PlayerActivity.this.buildMediaSource(uri);
+ return PlayerActivity.this.createLeafMediaSource(
+ uri, /* extension=*/ null, DrmSessionManager.getDummyDrmSessionManager());
}
@Override
@@ -609,7 +627,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);
+ return new AdsMediaSource(mediaSource, adMediaSourceFactory, adsLoader, playerView);
} catch (ClassNotFoundException e) {
// IMA extension not loaded.
return null;
@@ -620,41 +638,9 @@ public class PlayerActivity extends Activity
// User controls
- private void updateButtonVisibilities() {
- debugRootView.removeAllViews();
- if (player == null) {
- return;
- }
-
- MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
- if (mappedTrackInfo == null) {
- return;
- }
-
- 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.exo_track_selection_title_audio;
- break;
- case C.TRACK_TYPE_VIDEO:
- label = R.string.exo_track_selection_title_video;
- break;
- case C.TRACK_TYPE_TEXT:
- label = R.string.exo_track_selection_title_text;
- break;
- default:
- continue;
- }
- button.setText(label);
- button.setTag(i);
- button.setOnClickListener(this);
- debugRootView.addView(button);
- }
- }
+ private void updateButtonVisibility() {
+ selectTracksButton.setEnabled(
+ player != null && TrackSelectionDialog.willHaveContent(trackSelector));
}
private void showControls() {
@@ -686,20 +672,11 @@ public class PlayerActivity extends Activity
private class PlayerEventListener implements Player.EventListener {
@Override
- public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
+ public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
if (playbackState == Player.STATE_ENDED) {
showControls();
}
- updateButtonVisibilities();
- }
-
- @Override
- public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
- 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();
- }
+ updateButtonVisibility();
}
@Override
@@ -708,8 +685,7 @@ public class PlayerActivity extends Activity
clearStartPosition();
initializePlayer();
} else {
- updateStartPosition();
- updateButtonVisibilities();
+ updateButtonVisibility();
showControls();
}
}
@@ -717,7 +693,7 @@ public class PlayerActivity extends Activity
@Override
@SuppressWarnings("ReferenceEquality")
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
- updateButtonVisibilities();
+ updateButtonVisibility();
if (trackGroups != lastSeenTrackGroupArray) {
MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
if (mappedTrackInfo != null) {
@@ -746,7 +722,7 @@ public class PlayerActivity extends Activity
// Special case for decoder initialization failures.
DecoderInitializationException decoderInitializationException =
(DecoderInitializationException) cause;
- if (decoderInitializationException.decoderName == null) {
+ if (decoderInitializationException.codecInfo == null) {
if (decoderInitializationException.getCause() instanceof DecoderQueryException) {
errorString = getString(R.string.error_querying_decoders);
} else if (decoderInitializationException.secureDecoderRequired) {
@@ -761,12 +737,11 @@ public class PlayerActivity extends Activity
errorString =
getString(
R.string.error_instantiating_decoder,
- decoderInitializationException.decoderName);
+ decoderInitializationException.codecInfo.name);
}
}
}
return Pair.create(0, errorString);
}
}
-
}
diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/Sample.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/Sample.java
new file mode 100644
index 0000000000..4497b9a984
--- /dev/null
+++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/Sample.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2019 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 static com.google.android.exoplayer2.demo.PlayerActivity.ACTION_VIEW_LIST;
+import static com.google.android.exoplayer2.demo.PlayerActivity.AD_TAG_URI_EXTRA;
+import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_KEY_REQUEST_PROPERTIES_EXTRA;
+import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_LICENSE_URL_EXTRA;
+import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_MULTI_SESSION_EXTRA;
+import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_SCHEME_EXTRA;
+import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_SCHEME_UUID_EXTRA;
+import static com.google.android.exoplayer2.demo.PlayerActivity.EXTENSION_EXTRA;
+import static com.google.android.exoplayer2.demo.PlayerActivity.URI_EXTRA;
+
+import android.content.Intent;
+import android.net.Uri;
+import androidx.annotation.Nullable;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+import java.util.ArrayList;
+import java.util.UUID;
+
+/* package */ abstract class Sample {
+
+ public static final class UriSample extends Sample {
+
+ public static UriSample createFromIntent(Uri uri, Intent intent, String extrasKeySuffix) {
+ String extension = intent.getStringExtra(EXTENSION_EXTRA + extrasKeySuffix);
+ String adsTagUriString = intent.getStringExtra(AD_TAG_URI_EXTRA + extrasKeySuffix);
+ Uri adTagUri = adsTagUriString != null ? Uri.parse(adsTagUriString) : null;
+ return new UriSample(
+ /* name= */ null,
+ DrmInfo.createFromIntent(intent, extrasKeySuffix),
+ uri,
+ extension,
+ adTagUri,
+ /* sphericalStereoMode= */ null);
+ }
+
+ public final Uri uri;
+ public final String extension;
+ public final DrmInfo drmInfo;
+ public final Uri adTagUri;
+ public final String sphericalStereoMode;
+
+ public UriSample(
+ String name,
+ DrmInfo drmInfo,
+ Uri uri,
+ String extension,
+ Uri adTagUri,
+ String sphericalStereoMode) {
+ super(name);
+ this.uri = uri;
+ this.extension = extension;
+ this.drmInfo = drmInfo;
+ this.adTagUri = adTagUri;
+ this.sphericalStereoMode = sphericalStereoMode;
+ }
+
+ @Override
+ public void addToIntent(Intent intent) {
+ intent.setAction(PlayerActivity.ACTION_VIEW).setData(uri);
+ intent.putExtra(PlayerActivity.SPHERICAL_STEREO_MODE_EXTRA, sphericalStereoMode);
+ addPlayerConfigToIntent(intent, /* extrasKeySuffix= */ "");
+ }
+
+ public void addToPlaylistIntent(Intent intent, String extrasKeySuffix) {
+ intent.putExtra(PlayerActivity.URI_EXTRA + extrasKeySuffix, uri.toString());
+ addPlayerConfigToIntent(intent, extrasKeySuffix);
+ }
+
+ private void addPlayerConfigToIntent(Intent intent, String extrasKeySuffix) {
+ intent
+ .putExtra(EXTENSION_EXTRA + extrasKeySuffix, extension)
+ .putExtra(
+ AD_TAG_URI_EXTRA + extrasKeySuffix, adTagUri != null ? adTagUri.toString() : null);
+ if (drmInfo != null) {
+ drmInfo.addToIntent(intent, extrasKeySuffix);
+ }
+ }
+ }
+
+ public static final class PlaylistSample extends Sample {
+
+ public final UriSample[] children;
+
+ public PlaylistSample(String name, UriSample... children) {
+ super(name);
+ this.children = children;
+ }
+
+ @Override
+ public void addToIntent(Intent intent) {
+ intent.setAction(PlayerActivity.ACTION_VIEW_LIST);
+ for (int i = 0; i < children.length; i++) {
+ children[i].addToPlaylistIntent(intent, /* extrasKeySuffix= */ "_" + i);
+ }
+ }
+ }
+
+ public static final class DrmInfo {
+
+ public static DrmInfo createFromIntent(Intent intent, String extrasKeySuffix) {
+ String schemeKey = DRM_SCHEME_EXTRA + extrasKeySuffix;
+ String schemeUuidKey = DRM_SCHEME_UUID_EXTRA + extrasKeySuffix;
+ if (!intent.hasExtra(schemeKey) && !intent.hasExtra(schemeUuidKey)) {
+ return null;
+ }
+ String drmSchemeExtra =
+ intent.hasExtra(schemeKey)
+ ? intent.getStringExtra(schemeKey)
+ : intent.getStringExtra(schemeUuidKey);
+ UUID drmScheme = Util.getDrmUuid(drmSchemeExtra);
+ String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL_EXTRA + extrasKeySuffix);
+ String[] keyRequestPropertiesArray =
+ intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA + extrasKeySuffix);
+ boolean drmMultiSession =
+ intent.getBooleanExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, false);
+ return new DrmInfo(drmScheme, drmLicenseUrl, keyRequestPropertiesArray, drmMultiSession);
+ }
+
+ public final UUID drmScheme;
+ public final String drmLicenseUrl;
+ public final String[] drmKeyRequestProperties;
+ public final boolean drmMultiSession;
+
+ public DrmInfo(
+ UUID drmScheme,
+ String drmLicenseUrl,
+ String[] drmKeyRequestProperties,
+ boolean drmMultiSession) {
+ this.drmScheme = drmScheme;
+ this.drmLicenseUrl = drmLicenseUrl;
+ this.drmKeyRequestProperties = drmKeyRequestProperties;
+ this.drmMultiSession = drmMultiSession;
+ }
+
+ public void addToIntent(Intent intent, String extrasKeySuffix) {
+ Assertions.checkNotNull(intent);
+ intent.putExtra(DRM_SCHEME_EXTRA + extrasKeySuffix, drmScheme.toString());
+ intent.putExtra(DRM_LICENSE_URL_EXTRA + extrasKeySuffix, drmLicenseUrl);
+ intent.putExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA + extrasKeySuffix, drmKeyRequestProperties);
+ intent.putExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, drmMultiSession);
+ }
+ }
+
+ public static Sample createFromIntent(Intent intent) {
+ if (ACTION_VIEW_LIST.equals(intent.getAction())) {
+ ArrayList intentUris = new ArrayList<>();
+ int index = 0;
+ while (intent.hasExtra(URI_EXTRA + "_" + index)) {
+ intentUris.add(intent.getStringExtra(URI_EXTRA + "_" + index));
+ index++;
+ }
+ UriSample[] children = new UriSample[intentUris.size()];
+ for (int i = 0; i < children.length; i++) {
+ Uri uri = Uri.parse(intentUris.get(i));
+ children[i] = UriSample.createFromIntent(uri, intent, /* extrasKeySuffix= */ "_" + i);
+ }
+ return new PlaylistSample(/* name= */ null, children);
+ } else {
+ return UriSample.createFromIntent(intent.getData(), intent, /* extrasKeySuffix= */ "");
+ }
+ }
+
+ @Nullable public final String name;
+
+ public Sample(String name) {
+ this.name = name;
+ }
+
+ public abstract void addToIntent(Intent intent);
+}
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 6817fab780..09fa62e51a 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
@@ -15,16 +15,15 @@
*/
package com.google.android.exoplayer2.demo;
-import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.AssetManager;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
import android.util.JsonReader;
-import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
@@ -38,12 +37,17 @@ import android.widget.ImageButton;
import android.widget.TextView;
import android.widget.Toast;
import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.RenderersFactory;
+import com.google.android.exoplayer2.demo.Sample.DrmInfo;
+import com.google.android.exoplayer2.demo.Sample.PlaylistSample;
+import com.google.android.exoplayer2.demo.Sample.UriSample;
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;
import com.google.android.exoplayer2.upstream.DefaultDataSource;
import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.io.InputStream;
@@ -54,7 +58,7 @@ import java.util.Collections;
import java.util.List;
/** An activity for selecting from a list of media samples. */
-public class SampleChooserActivity extends Activity
+public class SampleChooserActivity extends AppCompatActivity
implements DownloadTracker.Listener, OnChildClickListener {
private static final String TAG = "SampleChooserActivity";
@@ -160,13 +164,17 @@ public class SampleChooserActivity extends Activity
public boolean onChildClick(
ExpandableListView parent, View view, int groupPosition, int childPosition, long id) {
Sample sample = (Sample) view.getTag();
- startActivity(
- sample.buildIntent(
- /* context= */ this,
- isNonNullAndChecked(preferExtensionDecodersMenuItem),
- isNonNullAndChecked(randomAbrMenuItem)
- ? PlayerActivity.ABR_ALGORITHM_RANDOM
- : PlayerActivity.ABR_ALGORITHM_DEFAULT));
+ Intent intent = new Intent(this, PlayerActivity.class);
+ intent.putExtra(
+ PlayerActivity.PREFER_EXTENSION_DECODERS_EXTRA,
+ isNonNullAndChecked(preferExtensionDecodersMenuItem));
+ String abrAlgorithm =
+ isNonNullAndChecked(randomAbrMenuItem)
+ ? PlayerActivity.ABR_ALGORITHM_RANDOM
+ : PlayerActivity.ABR_ALGORITHM_DEFAULT;
+ intent.putExtra(PlayerActivity.ABR_ALGORITHM_EXTRA, abrAlgorithm);
+ sample.addToIntent(intent);
+ startActivity(intent);
return true;
}
@@ -177,7 +185,15 @@ public class SampleChooserActivity extends Activity
.show();
} else {
UriSample uriSample = (UriSample) sample;
- downloadTracker.toggleDownload(this, sample.name, uriSample.uri, uriSample.extension);
+ RenderersFactory renderersFactory =
+ ((DemoApplication) getApplication())
+ .buildRenderersFactory(isNonNullAndChecked(preferExtensionDecodersMenuItem));
+ downloadTracker.toggleDownload(
+ getSupportFragmentManager(),
+ sample.name,
+ uriSample.uri,
+ uriSample.extension,
+ renderersFactory);
}
}
@@ -300,17 +316,12 @@ public class SampleChooserActivity extends Activity
extension = reader.nextString();
break;
case "drm_scheme":
- Assertions.checkState(!insidePlaylist, "Invalid attribute on nested item: drm_scheme");
drmScheme = reader.nextString();
break;
case "drm_license_url":
- Assertions.checkState(!insidePlaylist,
- "Invalid attribute on nested item: drm_license_url");
drmLicenseUrl = reader.nextString();
break;
case "drm_key_request_properties":
- Assertions.checkState(!insidePlaylist,
- "Invalid attribute on nested item: drm_key_request_properties");
ArrayList drmKeyRequestPropertiesList = new ArrayList<>();
reader.beginObject();
while (reader.hasNext()) {
@@ -348,18 +359,21 @@ public class SampleChooserActivity extends Activity
DrmInfo drmInfo =
drmScheme == null
? null
- : new DrmInfo(drmScheme, drmLicenseUrl, drmKeyRequestProperties, drmMultiSession);
+ : new DrmInfo(
+ Util.getDrmUuid(drmScheme),
+ drmLicenseUrl,
+ drmKeyRequestProperties,
+ drmMultiSession);
if (playlistSamples != null) {
- UriSample[] playlistSamplesArray = playlistSamples.toArray(
- new UriSample[playlistSamples.size()]);
- return new PlaylistSample(sampleName, drmInfo, playlistSamplesArray);
+ UriSample[] playlistSamplesArray = playlistSamples.toArray(new UriSample[0]);
+ return new PlaylistSample(sampleName, playlistSamplesArray);
} else {
return new UriSample(
sampleName,
drmInfo,
uri,
extension,
- adTagUri,
+ adTagUri != null ? Uri.parse(adTagUri) : null,
sphericalStereoMode);
}
}
@@ -489,116 +503,4 @@ public class SampleChooserActivity extends Activity
}
}
-
- private static final class DrmInfo {
- public final String drmScheme;
- public final String drmLicenseUrl;
- public final String[] drmKeyRequestProperties;
- public final boolean drmMultiSession;
-
- public DrmInfo(
- String drmScheme,
- String drmLicenseUrl,
- String[] drmKeyRequestProperties,
- boolean drmMultiSession) {
- this.drmScheme = drmScheme;
- this.drmLicenseUrl = drmLicenseUrl;
- this.drmKeyRequestProperties = drmKeyRequestProperties;
- this.drmMultiSession = drmMultiSession;
- }
-
- public void updateIntent(Intent intent) {
- Assertions.checkNotNull(intent);
- 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 DrmInfo drmInfo;
-
- public Sample(String name, DrmInfo drmInfo) {
- this.name = name;
- this.drmInfo = drmInfo;
- }
-
- public Intent buildIntent(
- Context context, boolean preferExtensionDecoders, String abrAlgorithm) {
- Intent intent = new Intent(context, PlayerActivity.class);
- intent.putExtra(PlayerActivity.PREFER_EXTENSION_DECODERS_EXTRA, preferExtensionDecoders);
- intent.putExtra(PlayerActivity.ABR_ALGORITHM_EXTRA, abrAlgorithm);
- if (drmInfo != null) {
- drmInfo.updateIntent(intent);
- }
- return intent;
- }
-
- }
-
- private static final class UriSample extends Sample {
-
- public final Uri uri;
- public final String extension;
- public final String adTagUri;
- public final String sphericalStereoMode;
-
- public UriSample(
- String name,
- DrmInfo drmInfo,
- Uri uri,
- String extension,
- String adTagUri,
- String sphericalStereoMode) {
- super(name, drmInfo);
- this.uri = uri;
- this.extension = extension;
- this.adTagUri = adTagUri;
- this.sphericalStereoMode = sphericalStereoMode;
- }
-
- @Override
- public Intent buildIntent(
- Context context, boolean preferExtensionDecoders, String abrAlgorithm) {
- return super.buildIntent(context, preferExtensionDecoders, abrAlgorithm)
- .setData(uri)
- .putExtra(PlayerActivity.EXTENSION_EXTRA, extension)
- .putExtra(PlayerActivity.AD_TAG_URI_EXTRA, adTagUri)
- .putExtra(PlayerActivity.SPHERICAL_STEREO_MODE_EXTRA, sphericalStereoMode)
- .setAction(PlayerActivity.ACTION_VIEW);
- }
-
- }
-
- private static final class PlaylistSample extends Sample {
-
- public final UriSample[] children;
-
- public PlaylistSample(
- String name,
- DrmInfo drmInfo,
- UriSample... children) {
- super(name, drmInfo);
- this.children = children;
- }
-
- @Override
- public Intent buildIntent(
- Context context, boolean preferExtensionDecoders, String abrAlgorithm) {
- 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.toString();
- extensions[i] = children[i].extension;
- }
- return super.buildIntent(context, preferExtensionDecoders, abrAlgorithm)
- .putExtra(PlayerActivity.URI_LIST_EXTRA, uris)
- .putExtra(PlayerActivity.EXTENSION_LIST_EXTRA, extensions)
- .setAction(PlayerActivity.ACTION_VIEW_LIST);
- }
-
- }
-
}
diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java
new file mode 100644
index 0000000000..d6fe6e2dc1
--- /dev/null
+++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java
@@ -0,0 +1,368 @@
+/*
+ * Copyright (C) 2019 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.Dialog;
+import android.content.DialogInterface;
+import android.content.res.Resources;
+import android.os.Bundle;
+import androidx.annotation.Nullable;
+import com.google.android.material.tabs.TabLayout;
+import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+import androidx.fragment.app.FragmentPagerAdapter;
+import androidx.appcompat.app.AppCompatDialog;
+import android.util.SparseArray;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import androidx.viewpager.widget.ViewPager;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
+import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride;
+import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
+import com.google.android.exoplayer2.ui.TrackSelectionView;
+import com.google.android.exoplayer2.util.Assertions;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/** Dialog to select tracks. */
+public final class TrackSelectionDialog extends DialogFragment {
+
+ private final SparseArray tabFragments;
+ private final ArrayList tabTrackTypes;
+
+ private int titleId;
+ private DialogInterface.OnClickListener onClickListener;
+ private DialogInterface.OnDismissListener onDismissListener;
+
+ /**
+ * Returns whether a track selection dialog will have content to display if initialized with the
+ * specified {@link DefaultTrackSelector} in its current state.
+ */
+ public static boolean willHaveContent(DefaultTrackSelector trackSelector) {
+ MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
+ return mappedTrackInfo != null && willHaveContent(mappedTrackInfo);
+ }
+
+ /**
+ * Returns whether a track selection dialog will have content to display if initialized with the
+ * specified {@link MappedTrackInfo}.
+ */
+ public static boolean willHaveContent(MappedTrackInfo mappedTrackInfo) {
+ for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) {
+ if (showTabForRenderer(mappedTrackInfo, i)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Creates a dialog for a given {@link DefaultTrackSelector}, whose parameters will be
+ * automatically updated when tracks are selected.
+ *
+ * @param trackSelector The {@link DefaultTrackSelector}.
+ * @param onDismissListener A {@link DialogInterface.OnDismissListener} to call when the dialog is
+ * dismissed.
+ */
+ public static TrackSelectionDialog createForTrackSelector(
+ DefaultTrackSelector trackSelector, DialogInterface.OnDismissListener onDismissListener) {
+ MappedTrackInfo mappedTrackInfo =
+ Assertions.checkNotNull(trackSelector.getCurrentMappedTrackInfo());
+ TrackSelectionDialog trackSelectionDialog = new TrackSelectionDialog();
+ DefaultTrackSelector.Parameters parameters = trackSelector.getParameters();
+ trackSelectionDialog.init(
+ /* titleId= */ R.string.track_selection_title,
+ mappedTrackInfo,
+ /* initialParameters = */ parameters,
+ /* allowAdaptiveSelections =*/ true,
+ /* allowMultipleOverrides= */ false,
+ /* onClickListener= */ (dialog, which) -> {
+ DefaultTrackSelector.ParametersBuilder builder = parameters.buildUpon();
+ for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) {
+ builder
+ .clearSelectionOverrides(/* rendererIndex= */ i)
+ .setRendererDisabled(
+ /* rendererIndex= */ i,
+ trackSelectionDialog.getIsDisabled(/* rendererIndex= */ i));
+ List overrides =
+ trackSelectionDialog.getOverrides(/* rendererIndex= */ i);
+ if (!overrides.isEmpty()) {
+ builder.setSelectionOverride(
+ /* rendererIndex= */ i,
+ mappedTrackInfo.getTrackGroups(/* rendererIndex= */ i),
+ overrides.get(0));
+ }
+ }
+ trackSelector.setParameters(builder);
+ },
+ onDismissListener);
+ return trackSelectionDialog;
+ }
+
+ /**
+ * Creates a dialog for given {@link MappedTrackInfo} and {@link DefaultTrackSelector.Parameters}.
+ *
+ * @param titleId The resource id of the dialog title.
+ * @param mappedTrackInfo The {@link MappedTrackInfo} to display.
+ * @param initialParameters The {@link DefaultTrackSelector.Parameters} describing the initial
+ * track selection.
+ * @param allowAdaptiveSelections Whether adaptive selections (consisting of more than one track)
+ * can be made.
+ * @param allowMultipleOverrides Whether tracks from multiple track groups can be selected.
+ * @param onClickListener {@link DialogInterface.OnClickListener} called when tracks are selected.
+ * @param onDismissListener {@link DialogInterface.OnDismissListener} called when the dialog is
+ * dismissed.
+ */
+ public static TrackSelectionDialog createForMappedTrackInfoAndParameters(
+ int titleId,
+ MappedTrackInfo mappedTrackInfo,
+ DefaultTrackSelector.Parameters initialParameters,
+ boolean allowAdaptiveSelections,
+ boolean allowMultipleOverrides,
+ DialogInterface.OnClickListener onClickListener,
+ DialogInterface.OnDismissListener onDismissListener) {
+ TrackSelectionDialog trackSelectionDialog = new TrackSelectionDialog();
+ trackSelectionDialog.init(
+ titleId,
+ mappedTrackInfo,
+ initialParameters,
+ allowAdaptiveSelections,
+ allowMultipleOverrides,
+ onClickListener,
+ onDismissListener);
+ return trackSelectionDialog;
+ }
+
+ public TrackSelectionDialog() {
+ tabFragments = new SparseArray<>();
+ tabTrackTypes = new ArrayList<>();
+ // Retain instance across activity re-creation to prevent losing access to init data.
+ setRetainInstance(true);
+ }
+
+ private void init(
+ int titleId,
+ MappedTrackInfo mappedTrackInfo,
+ DefaultTrackSelector.Parameters initialParameters,
+ boolean allowAdaptiveSelections,
+ boolean allowMultipleOverrides,
+ DialogInterface.OnClickListener onClickListener,
+ DialogInterface.OnDismissListener onDismissListener) {
+ this.titleId = titleId;
+ this.onClickListener = onClickListener;
+ this.onDismissListener = onDismissListener;
+ for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) {
+ if (showTabForRenderer(mappedTrackInfo, i)) {
+ int trackType = mappedTrackInfo.getRendererType(/* rendererIndex= */ i);
+ TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(i);
+ TrackSelectionViewFragment tabFragment = new TrackSelectionViewFragment();
+ tabFragment.init(
+ mappedTrackInfo,
+ /* rendererIndex= */ i,
+ initialParameters.getRendererDisabled(/* rendererIndex= */ i),
+ initialParameters.getSelectionOverride(/* rendererIndex= */ i, trackGroupArray),
+ allowAdaptiveSelections,
+ allowMultipleOverrides);
+ tabFragments.put(i, tabFragment);
+ tabTrackTypes.add(trackType);
+ }
+ }
+ }
+
+ /**
+ * Returns whether a renderer is disabled.
+ *
+ * @param rendererIndex Renderer index.
+ * @return Whether the renderer is disabled.
+ */
+ public boolean getIsDisabled(int rendererIndex) {
+ TrackSelectionViewFragment rendererView = tabFragments.get(rendererIndex);
+ return rendererView != null && rendererView.isDisabled;
+ }
+
+ /**
+ * Returns the list of selected track selection overrides for the specified renderer. There will
+ * be at most one override for each track group.
+ *
+ * @param rendererIndex Renderer index.
+ * @return The list of track selection overrides for this renderer.
+ */
+ public List getOverrides(int rendererIndex) {
+ TrackSelectionViewFragment rendererView = tabFragments.get(rendererIndex);
+ return rendererView == null ? Collections.emptyList() : rendererView.overrides;
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ // We need to own the view to let tab layout work correctly on all API levels. We can't use
+ // AlertDialog because it owns the view itself, so we use AppCompatDialog instead, themed using
+ // the AlertDialog theme overlay with force-enabled title.
+ AppCompatDialog dialog =
+ new AppCompatDialog(getActivity(), R.style.TrackSelectionDialogThemeOverlay);
+ dialog.setTitle(titleId);
+ return dialog;
+ }
+
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ super.onDismiss(dialog);
+ onDismissListener.onDismiss(dialog);
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+
+ View dialogView = inflater.inflate(R.layout.track_selection_dialog, container, false);
+ TabLayout tabLayout = dialogView.findViewById(R.id.track_selection_dialog_tab_layout);
+ ViewPager viewPager = dialogView.findViewById(R.id.track_selection_dialog_view_pager);
+ Button cancelButton = dialogView.findViewById(R.id.track_selection_dialog_cancel_button);
+ Button okButton = dialogView.findViewById(R.id.track_selection_dialog_ok_button);
+ viewPager.setAdapter(new FragmentAdapter(getChildFragmentManager()));
+ tabLayout.setupWithViewPager(viewPager);
+ tabLayout.setVisibility(tabFragments.size() > 1 ? View.VISIBLE : View.GONE);
+ cancelButton.setOnClickListener(view -> dismiss());
+ okButton.setOnClickListener(
+ view -> {
+ onClickListener.onClick(getDialog(), DialogInterface.BUTTON_POSITIVE);
+ dismiss();
+ });
+ return dialogView;
+ }
+
+ private static boolean showTabForRenderer(MappedTrackInfo mappedTrackInfo, int rendererIndex) {
+ TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(rendererIndex);
+ if (trackGroupArray.length == 0) {
+ return false;
+ }
+ int trackType = mappedTrackInfo.getRendererType(rendererIndex);
+ return isSupportedTrackType(trackType);
+ }
+
+ private static boolean isSupportedTrackType(int trackType) {
+ switch (trackType) {
+ case C.TRACK_TYPE_VIDEO:
+ case C.TRACK_TYPE_AUDIO:
+ case C.TRACK_TYPE_TEXT:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ private static String getTrackTypeString(Resources resources, int trackType) {
+ switch (trackType) {
+ case C.TRACK_TYPE_VIDEO:
+ return resources.getString(R.string.exo_track_selection_title_video);
+ case C.TRACK_TYPE_AUDIO:
+ return resources.getString(R.string.exo_track_selection_title_audio);
+ case C.TRACK_TYPE_TEXT:
+ return resources.getString(R.string.exo_track_selection_title_text);
+ default:
+ throw new IllegalArgumentException();
+ }
+ }
+
+ private final class FragmentAdapter extends FragmentPagerAdapter {
+
+ public FragmentAdapter(FragmentManager fragmentManager) {
+ super(fragmentManager);
+ }
+
+ @Override
+ public Fragment getItem(int position) {
+ return tabFragments.valueAt(position);
+ }
+
+ @Override
+ public int getCount() {
+ return tabFragments.size();
+ }
+
+ @Nullable
+ @Override
+ public CharSequence getPageTitle(int position) {
+ return getTrackTypeString(getResources(), tabTrackTypes.get(position));
+ }
+ }
+
+ /** Fragment to show a track selection in tab of the track selection dialog. */
+ public static final class TrackSelectionViewFragment extends Fragment
+ implements TrackSelectionView.TrackSelectionListener {
+
+ private MappedTrackInfo mappedTrackInfo;
+ private int rendererIndex;
+ private boolean allowAdaptiveSelections;
+ private boolean allowMultipleOverrides;
+
+ /* package */ boolean isDisabled;
+ /* package */ List overrides;
+
+ public TrackSelectionViewFragment() {
+ // Retain instance across activity re-creation to prevent losing access to init data.
+ setRetainInstance(true);
+ }
+
+ public void init(
+ MappedTrackInfo mappedTrackInfo,
+ int rendererIndex,
+ boolean initialIsDisabled,
+ @Nullable SelectionOverride initialOverride,
+ boolean allowAdaptiveSelections,
+ boolean allowMultipleOverrides) {
+ this.mappedTrackInfo = mappedTrackInfo;
+ this.rendererIndex = rendererIndex;
+ this.isDisabled = initialIsDisabled;
+ this.overrides =
+ initialOverride == null
+ ? Collections.emptyList()
+ : Collections.singletonList(initialOverride);
+ this.allowAdaptiveSelections = allowAdaptiveSelections;
+ this.allowMultipleOverrides = allowMultipleOverrides;
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater,
+ @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ View rootView =
+ inflater.inflate(
+ R.layout.exo_track_selection_dialog, container, /* attachToRoot= */ false);
+ TrackSelectionView trackSelectionView = rootView.findViewById(R.id.exo_track_selection_view);
+ trackSelectionView.setShowDisableOption(true);
+ trackSelectionView.setAllowMultipleOverrides(allowMultipleOverrides);
+ trackSelectionView.setAllowAdaptiveSelections(allowAdaptiveSelections);
+ trackSelectionView.init(
+ mappedTrackInfo, rendererIndex, isDisabled, overrides, /* listener= */ this);
+ return rootView;
+ }
+
+ @Override
+ public void onTrackSelectionChanged(boolean isDisabled, List overrides) {
+ this.isDisabled = isDisabled;
+ this.overrides = overrides;
+ }
+ }
+}
diff --git a/demos/main/src/main/res/drawable-xxhdpi/ic_download.png b/demos/main/src/main/res/drawable-xxhdpi/ic_download.png
index f02715177a..4e04a30198 100644
Binary files a/demos/main/src/main/res/drawable-xxhdpi/ic_download.png and b/demos/main/src/main/res/drawable-xxhdpi/ic_download.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
index 6602791545..f9bfb5edba 100644
Binary files a/demos/main/src/main/res/drawable-xxxhdpi/ic_download.png and b/demos/main/src/main/res/drawable-xxxhdpi/ic_download.png differ
diff --git a/demos/main/src/main/res/layout/player_activity.xml b/demos/main/src/main/res/layout/player_activity.xml
index 6b84033273..ea3de257e2 100644
--- a/demos/main/src/main/res/layout/player_activity.xml
+++ b/demos/main/src/main/res/layout/player_activity.xml
@@ -42,7 +42,15 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
- android:visibility="gone"/>
+ android:visibility="gone">
+
+
+
+
diff --git a/demos/main/src/main/res/layout/track_selection_dialog.xml b/demos/main/src/main/res/layout/track_selection_dialog.xml
new file mode 100644
index 0000000000..7f6c45e131
--- /dev/null
+++ b/demos/main/src/main/res/layout/track_selection_dialog.xml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/main/src/main/res/menu/sample_chooser_menu.xml b/demos/main/src/main/res/menu/sample_chooser_menu.xml
index 566b23a0d5..9934e9db95 100644
--- a/demos/main/src/main/res/menu/sample_chooser_menu.xml
+++ b/demos/main/src/main/res/menu/sample_chooser_menu.xml
@@ -13,13 +13,14 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
+
+ android:title="@string/prefer_extension_decoders"
+ android:checkable="true"
+ app:showAsAction="never"/>
+ android:title="@string/random_abr"
+ android:checkable="true"
+ app:showAsAction="never"/>
diff --git a/demos/main/src/main/res/values/strings.xml b/demos/main/src/main/res/values/strings.xml
index 40f065b18e..f74ce8c076 100644
--- a/demos/main/src/main/res/values/strings.xml
+++ b/demos/main/src/main/res/values/strings.xml
@@ -17,6 +17,8 @@
ExoPlayer
+ Select tracks
+
Unexpected intent action: %1$s
Cleartext traffic not permitted
@@ -51,6 +53,8 @@
Playing sample without ads, as the IMA extension was not loaded
+ Playing sample without ads, as ads are not supported in concatenations
+
Failed to start download
This demo app does not support downloading playlists
diff --git a/demos/main/src/main/res/values/styles.xml b/demos/main/src/main/res/values/styles.xml
index 25d826bdf6..04c5b90edc 100644
--- a/demos/main/src/main/res/values/styles.xml
+++ b/demos/main/src/main/res/values/styles.xml
@@ -15,8 +15,11 @@
-->
-
+
+
diff --git a/extensions/cast/README.md b/extensions/cast/README.md
index cc72c5f9bc..1c0d7ac56f 100644
--- a/extensions/cast/README.md
+++ b/extensions/cast/README.md
@@ -5,7 +5,7 @@
The cast extension is a [Player][] implementation that controls playback on a
Cast receiver app.
-[Player]: https://google.github.io/ExoPlayer/doc/reference/index.html?com/google/android/exoplayer2/Player.html
+[Player]: https://exoplayer.dev/doc/reference/index.html?com/google/android/exoplayer2/Player.html
## Getting the extension ##
diff --git a/extensions/cast/build.gradle b/extensions/cast/build.gradle
index bee73cac12..4af8f94c58 100644
--- a/extensions/cast/build.gradle
+++ b/extensions/cast/build.gradle
@@ -16,7 +16,6 @@ apply plugin: 'com.android.library'
android {
compileSdkVersion project.ext.compileSdkVersion
- buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@@ -24,30 +23,22 @@ android {
}
defaultConfig {
- minSdkVersion 14
+ minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
- consumerProguardFiles 'proguard-rules.txt'
}
+
+ testOptions.unitTests.includeAndroidResources = true
}
dependencies {
- api 'com.google.android.gms:play-services-cast-framework:16.0.1'
+ api 'com.google.android.gms:play-services-cast-framework:17.0.0'
+ implementation 'androidx.annotation:annotation:1.1.0'
implementation project(modulePrefix + 'library-core')
implementation project(modulePrefix + 'library-ui')
+ compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
+ compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion
testImplementation project(modulePrefix + 'testutils')
- testImplementation 'junit:junit:' + junitVersion
- testImplementation 'org.mockito:mockito-core:' + mockitoVersion
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
- testImplementation project(modulePrefix + 'testutils-robolectric')
- // These dependencies are necessary to force the supportLibraryVersion of
- // 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 via:
- // com.google.android.gms:play-services-cast-framework:15.0.1
- // |-- com.android.support:mediarouter-v7:26.1.0
- api 'com.android.support:support-v4:' + supportLibraryVersion
- api 'com.android.support:mediarouter-v7:' + supportLibraryVersion
- api 'com.android.support:recyclerview-v7:' + supportLibraryVersion
}
ext {
diff --git a/extensions/cast/proguard-rules.txt b/extensions/cast/proguard-rules.txt
deleted file mode 100644
index bc94b33c1c..0000000000
--- a/extensions/cast/proguard-rules.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-# Proguard rules specific to the Cast extension.
-
-# DefaultCastOptionsProvider is commonly referred to only by the app's manifest.
--keep class com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider
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 65ae097452..6a33aa0428 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
@@ -15,9 +15,10 @@
*/
package com.google.android.exoplayer2.ext.cast;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.util.Log;
+import android.os.Looper;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.google.android.exoplayer2.BasePlayer;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.PlaybackParameters;
@@ -29,8 +30,8 @@ import com.google.android.exoplayer2.trackselection.FixedTrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.MimeTypes;
-import com.google.android.exoplayer2.util.Util;
import com.google.android.gms.cast.CastStatusCodes;
import com.google.android.gms.cast.MediaInfo;
import com.google.android.gms.cast.MediaQueueItem;
@@ -44,41 +45,27 @@ import com.google.android.gms.cast.framework.media.RemoteMediaClient;
import com.google.android.gms.cast.framework.media.RemoteMediaClient.MediaChannelResult;
import com.google.android.gms.common.api.PendingResult;
import com.google.android.gms.common.api.ResultCallback;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Iterator;
import java.util.List;
-import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.concurrent.CopyOnWriteArrayList;
/**
* {@link Player} implementation that communicates with a Cast receiver app.
*
* The behavior of this class depends on the underlying Cast session, which is obtained from the
- * Cast context passed to {@link #CastPlayer}. To keep track of the session,
- * {@link #isCastSessionAvailable()} can be queried and {@link SessionAvailabilityListener} can be
- * implemented and attached to the player.
+ * Cast context passed to {@link #CastPlayer}. To keep track of the session, {@link
+ * #isCastSessionAvailable()} can be queried and {@link SessionAvailabilityListener} can be
+ * implemented and attached to the player.
*
- * If no session is available, the player state will remain unchanged and calls to methods that
+ *
If no session is available, the player state will remain unchanged and calls to methods that
* alter it will be ignored. Querying the player state is possible even when no session is
- * available, in which case, the last observed receiver app state is reported.
+ * available, in which case, the last observed receiver app state is reported.
*
- * Methods should be called on the application's main thread.
+ * Methods should be called on the application's main thread.
*/
-public final class CastPlayer implements Player {
-
- /**
- * Listener of changes in the cast session availability.
- */
- public interface SessionAvailabilityListener {
-
- /**
- * Called when a cast session becomes available to the player.
- */
- void onCastSessionAvailable();
-
- /**
- * Called when the cast session becomes unavailable.
- */
- void onCastSessionUnavailable();
-
- }
+public final class CastPlayer extends BasePlayer {
private static final String TAG = "CastPlayer";
@@ -94,24 +81,24 @@ public final class CastPlayer implements Player {
private final CastContext castContext;
// TODO: Allow custom implementations of CastTimelineTracker.
private final CastTimelineTracker timelineTracker;
- private final Timeline.Window window;
private final Timeline.Period period;
- private RemoteMediaClient remoteMediaClient;
-
// Result callbacks.
private final StatusListener statusListener;
private final SeekResultCallback seekResultCallback;
- // Listeners.
- private final CopyOnWriteArraySet listeners;
- private SessionAvailabilityListener sessionAvailabilityListener;
+ // Listeners and notification.
+ private final CopyOnWriteArrayList listeners;
+ private final ArrayList notificationsBatch;
+ private final ArrayDeque ongoingNotificationsTasks;
+ @Nullable private SessionAvailabilityListener sessionAvailabilityListener;
// Internal state.
+ @Nullable private RemoteMediaClient remoteMediaClient;
private CastTimeline currentTimeline;
private TrackGroupArray currentTrackGroups;
private TrackSelectionArray currentTrackSelection;
- private int playbackState;
+ @Player.State private int playbackState;
private int repeatMode;
private int currentWindowIndex;
private boolean playWhenReady;
@@ -127,11 +114,12 @@ public final class CastPlayer implements Player {
public CastPlayer(CastContext castContext) {
this.castContext = castContext;
timelineTracker = new CastTimelineTracker();
- window = new Timeline.Window();
period = new Timeline.Period();
statusListener = new StatusListener();
seekResultCallback = new SeekResultCallback();
- listeners = new CopyOnWriteArraySet<>();
+ listeners = new CopyOnWriteArrayList<>();
+ notificationsBatch = new ArrayList<>();
+ ongoingNotificationsTasks = new ArrayDeque<>();
SessionManager sessionManager = castContext.getSessionManager();
sessionManager.addSessionManagerListener(statusListener, CastSession.class);
@@ -159,6 +147,7 @@ public final class CastPlayer implements Player {
* starts at position 0.
* @return The Cast {@code PendingResult}, or null if no session is available.
*/
+ @Nullable
public PendingResult loadItem(MediaQueueItem item, long positionMs) {
return loadItems(new MediaQueueItem[] {item}, 0, positionMs, REPEAT_MODE_OFF);
}
@@ -174,8 +163,9 @@ public final class CastPlayer implements Player {
* @param repeatMode The repeat mode for the created media queue.
* @return The Cast {@code PendingResult}, or null if no session is available.
*/
- public PendingResult loadItems(MediaQueueItem[] items, int startIndex,
- long positionMs, @RepeatMode int repeatMode) {
+ @Nullable
+ public PendingResult loadItems(
+ MediaQueueItem[] items, int startIndex, long positionMs, @RepeatMode int repeatMode) {
if (remoteMediaClient != null) {
positionMs = positionMs != C.TIME_UNSET ? positionMs : 0;
waitingForInitialTimeline = true;
@@ -191,6 +181,7 @@ public final class CastPlayer implements Player {
* @param items The items to append.
* @return The Cast {@code PendingResult}, or null if no media queue exists.
*/
+ @Nullable
public PendingResult addItems(MediaQueueItem... items) {
return addItems(MediaQueueItem.INVALID_ITEM_ID, items);
}
@@ -205,6 +196,7 @@ public final class CastPlayer implements Player {
* @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code
* periodId} exist.
*/
+ @Nullable
public PendingResult addItems(int periodId, MediaQueueItem... items) {
if (getMediaStatus() != null && (periodId == MediaQueueItem.INVALID_ITEM_ID
|| currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET)) {
@@ -222,6 +214,7 @@ public final class CastPlayer implements Player {
* @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code
* periodId} exist.
*/
+ @Nullable
public PendingResult removeItem(int periodId) {
if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) {
return remoteMediaClient.queueRemoveItem(periodId, null);
@@ -240,6 +233,7 @@ public final class CastPlayer implements Player {
* @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code
* periodId} exist.
*/
+ @Nullable
public PendingResult moveItem(int periodId, int newIndex) {
Assertions.checkArgument(newIndex >= 0 && newIndex < currentTimeline.getPeriodCount());
if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) {
@@ -257,6 +251,7 @@ public final class CastPlayer implements Player {
* @return The item that corresponds to the period with the given id, or null if no media queue or
* period with id {@code periodId} exist.
*/
+ @Nullable
public MediaQueueItem getItem(int periodId) {
MediaStatus mediaStatus = getMediaStatus();
return mediaStatus != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET
@@ -275,45 +270,66 @@ public final class CastPlayer implements Player {
/**
* Sets a listener for updates on the cast session availability.
*
- * @param listener The {@link SessionAvailabilityListener}.
+ * @param listener The {@link SessionAvailabilityListener}, or null to clear the listener.
*/
- public void setSessionAvailabilityListener(SessionAvailabilityListener listener) {
+ public void setSessionAvailabilityListener(@Nullable SessionAvailabilityListener listener) {
sessionAvailabilityListener = listener;
}
// Player implementation.
@Override
+ @Nullable
public AudioComponent getAudioComponent() {
return null;
}
@Override
+ @Nullable
public VideoComponent getVideoComponent() {
return null;
}
@Override
+ @Nullable
public TextComponent getTextComponent() {
return null;
}
+ @Override
+ @Nullable
+ public MetadataComponent getMetadataComponent() {
+ return null;
+ }
+
+ @Override
+ public Looper getApplicationLooper() {
+ return Looper.getMainLooper();
+ }
+
@Override
public void addListener(EventListener listener) {
- listeners.add(listener);
+ listeners.addIfAbsent(new ListenerHolder(listener));
}
@Override
public void removeListener(EventListener listener) {
- listeners.remove(listener);
+ for (ListenerHolder listenerHolder : listeners) {
+ if (listenerHolder.listener.equals(listener)) {
+ listenerHolder.release();
+ listeners.remove(listenerHolder);
+ }
+ }
}
@Override
+ @Player.State
public int getPlaybackState() {
return playbackState;
}
@Override
+ @Nullable
public ExoPlaybackException getPlaybackError() {
return null;
}
@@ -335,21 +351,6 @@ public final class CastPlayer implements Player {
return playWhenReady;
}
- @Override
- public void seekToDefaultPosition() {
- seekTo(0);
- }
-
- @Override
- public void seekToDefaultPosition(int windowIndex) {
- seekTo(windowIndex, 0);
- }
-
- @Override
- public void seekTo(long positionMs) {
- seekTo(getCurrentWindowIndex(), positionMs);
- }
-
@Override
public void seekTo(int windowIndex, long positionMs) {
MediaStatus mediaStatus = getMediaStatus();
@@ -366,14 +367,13 @@ public final class CastPlayer implements Player {
pendingSeekCount++;
pendingSeekWindowIndex = windowIndex;
pendingSeekPositionMs = positionMs;
- for (EventListener listener : listeners) {
- listener.onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK);
- }
+ notificationsBatch.add(
+ new ListenerNotificationTask(
+ listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_SEEK)));
} else if (pendingSeekCount == 0) {
- for (EventListener listener : listeners) {
- listener.onSeekProcessed();
- }
+ notificationsBatch.add(new ListenerNotificationTask(EventListener::onSeekProcessed));
}
+ flushNotifications();
}
@Override
@@ -386,11 +386,6 @@ public final class CastPlayer implements Player {
return PlaybackParameters.DEFAULT;
}
- @Override
- public void stop() {
- stop(/* reset= */ false);
- }
-
@Override
public void stop(boolean reset) {
playbackState = STATE_IDLE;
@@ -465,11 +460,6 @@ public final class CastPlayer implements Player {
return currentTimeline;
}
- @Override
- @Nullable public Object getCurrentManifest() {
- return null;
- }
-
@Override
public int getCurrentPeriodIndex() {
return getCurrentWindowIndex();
@@ -480,32 +470,11 @@ public final class CastPlayer implements Player {
return pendingSeekWindowIndex != C.INDEX_UNSET ? pendingSeekWindowIndex : currentWindowIndex;
}
- @Override
- public int getNextWindowIndex() {
- return currentTimeline.isEmpty() ? C.INDEX_UNSET
- : currentTimeline.getNextWindowIndex(getCurrentWindowIndex(), repeatMode, false);
- }
-
- @Override
- public int getPreviousWindowIndex() {
- return currentTimeline.isEmpty() ? C.INDEX_UNSET
- : 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
public long getDuration() {
- return currentTimeline.isEmpty() ? C.TIME_UNSET
- : currentTimeline.getWindow(getCurrentWindowIndex(), window).getDurationMs();
+ return getContentDuration();
}
@Override
@@ -522,15 +491,6 @@ public final class CastPlayer implements Player {
return getCurrentPosition();
}
- @Override
- public int getBufferedPercentage() {
- long position = getBufferedPosition();
- long duration = getDuration();
- return position == C.TIME_UNSET || duration == C.TIME_UNSET
- ? 0
- : duration == 0 ? 100 : Util.constrainValue((int) ((position * 100) / duration), 0, 100);
- }
-
@Override
public long getTotalBufferedDuration() {
long bufferedPosition = getBufferedPosition();
@@ -540,18 +500,6 @@ public final class CastPlayer implements Player {
: bufferedPosition - currentPosition;
}
- @Override
- public boolean isCurrentWindowDynamic() {
- return !currentTimeline.isEmpty()
- && currentTimeline.getWindow(getCurrentWindowIndex(), window).isDynamic;
- }
-
- @Override
- public boolean isCurrentWindowSeekable() {
- return !currentTimeline.isEmpty()
- && currentTimeline.getWindow(getCurrentWindowIndex(), window).isSeekable;
- }
-
@Override
public boolean isPlayingAd() {
return false;
@@ -567,11 +515,6 @@ public final class CastPlayer implements Player {
return C.INDEX_UNSET;
}
- @Override
- public long getContentDuration() {
- return getDuration();
- }
-
@Override
public boolean isLoading() {
return false;
@@ -589,7 +532,7 @@ public final class CastPlayer implements Player {
// Internal methods.
- public void updateInternalState() {
+ private void updateInternalState() {
if (remoteMediaClient == null) {
// There is no session. We leave the state of the player as it is now.
return;
@@ -601,30 +544,40 @@ public final class CastPlayer implements Player {
|| this.playWhenReady != playWhenReady) {
this.playbackState = playbackState;
this.playWhenReady = playWhenReady;
- for (EventListener listener : listeners) {
- listener.onPlayerStateChanged(this.playWhenReady, this.playbackState);
- }
+ notificationsBatch.add(
+ new ListenerNotificationTask(
+ listener -> listener.onPlayerStateChanged(this.playWhenReady, this.playbackState)));
}
@RepeatMode int repeatMode = fetchRepeatMode(remoteMediaClient);
if (this.repeatMode != repeatMode) {
this.repeatMode = repeatMode;
- for (EventListener listener : listeners) {
- listener.onRepeatModeChanged(repeatMode);
- }
- }
- int currentWindowIndex = fetchCurrentWindowIndex(getMediaStatus());
- if (this.currentWindowIndex != currentWindowIndex && pendingSeekCount == 0) {
- this.currentWindowIndex = currentWindowIndex;
- for (EventListener listener : listeners) {
- listener.onPositionDiscontinuity(DISCONTINUITY_REASON_PERIOD_TRANSITION);
- }
- }
- if (updateTracksAndSelections()) {
- for (EventListener listener : listeners) {
- listener.onTracksChanged(currentTrackGroups, currentTrackSelection);
- }
+ notificationsBatch.add(
+ new ListenerNotificationTask(listener -> listener.onRepeatModeChanged(this.repeatMode)));
}
maybeUpdateTimelineAndNotify();
+
+ int currentWindowIndex = C.INDEX_UNSET;
+ MediaQueueItem currentItem = remoteMediaClient.getCurrentItem();
+ if (currentItem != null) {
+ currentWindowIndex = currentTimeline.getIndexOfPeriod(currentItem.getItemId());
+ }
+ if (currentWindowIndex == C.INDEX_UNSET) {
+ // The timeline is empty. Fall back to index 0, which is what ExoPlayer would do.
+ currentWindowIndex = 0;
+ }
+ if (this.currentWindowIndex != currentWindowIndex && pendingSeekCount == 0) {
+ this.currentWindowIndex = currentWindowIndex;
+ notificationsBatch.add(
+ new ListenerNotificationTask(
+ listener ->
+ listener.onPositionDiscontinuity(DISCONTINUITY_REASON_PERIOD_TRANSITION)));
+ }
+ if (updateTracksAndSelections()) {
+ notificationsBatch.add(
+ new ListenerNotificationTask(
+ listener -> listener.onTracksChanged(currentTrackGroups, currentTrackSelection)));
+ }
+ flushNotifications();
}
private void maybeUpdateTimelineAndNotify() {
@@ -632,9 +585,9 @@ public final class CastPlayer implements Player {
@Player.TimelineChangeReason int reason = waitingForInitialTimeline
? Player.TIMELINE_CHANGE_REASON_PREPARED : Player.TIMELINE_CHANGE_REASON_DYNAMIC;
waitingForInitialTimeline = false;
- for (EventListener listener : listeners) {
- listener.onTimelineChanged(currentTimeline, null, reason);
- }
+ notificationsBatch.add(
+ new ListenerNotificationTask(
+ listener -> listener.onTimelineChanged(currentTimeline, reason)));
}
}
@@ -645,7 +598,9 @@ public final class CastPlayer implements Player {
CastTimeline oldTimeline = currentTimeline;
MediaStatus status = getMediaStatus();
currentTimeline =
- status != null ? timelineTracker.getCastTimeline(status) : CastTimeline.EMPTY_CAST_TIMELINE;
+ status != null
+ ? timelineTracker.getCastTimeline(remoteMediaClient)
+ : CastTimeline.EMPTY_CAST_TIMELINE;
return !oldTimeline.equals(currentTimeline);
}
@@ -722,7 +677,8 @@ public final class CastPlayer implements Player {
}
}
- private @Nullable MediaStatus getMediaStatus() {
+ @Nullable
+ private MediaStatus getMediaStatus() {
return remoteMediaClient != null ? remoteMediaClient.getMediaStatus() : null;
}
@@ -770,16 +726,6 @@ public final class CastPlayer implements Player {
}
}
- /**
- * Retrieves the current item index from {@code mediaStatus} and maps it into a window index. If
- * there is no media session, returns 0.
- */
- private static int fetchCurrentWindowIndex(@Nullable MediaStatus mediaStatus) {
- Integer currentItemId = mediaStatus != null
- ? mediaStatus.getIndexById(mediaStatus.getCurrentItemId()) : null;
- return currentItemId != null ? currentItemId : 0;
- }
-
private static boolean isTrackActive(long id, long[] activeTrackIds) {
for (long activeTrackId : activeTrackIds) {
if (activeTrackId == id) {
@@ -895,7 +841,23 @@ public final class CastPlayer implements Player {
}
- // Result callbacks hooks.
+ // Internal methods.
+
+ private void flushNotifications() {
+ boolean recursiveNotification = !ongoingNotificationsTasks.isEmpty();
+ ongoingNotificationsTasks.addAll(notificationsBatch);
+ notificationsBatch.clear();
+ if (recursiveNotification) {
+ // This will be handled once the current notification task is finished.
+ return;
+ }
+ while (!ongoingNotificationsTasks.isEmpty()) {
+ ongoingNotificationsTasks.peekFirst().execute();
+ ongoingNotificationsTasks.removeFirst();
+ }
+ }
+
+ // Internal classes.
private final class SeekResultCallback implements ResultCallback {
@@ -909,9 +871,25 @@ public final class CastPlayer implements Player {
if (--pendingSeekCount == 0) {
pendingSeekWindowIndex = C.INDEX_UNSET;
pendingSeekPositionMs = C.TIME_UNSET;
- for (EventListener listener : listeners) {
- listener.onSeekProcessed();
- }
+ notificationsBatch.add(new ListenerNotificationTask(EventListener::onSeekProcessed));
+ flushNotifications();
+ }
+ }
+ }
+
+ private final class ListenerNotificationTask {
+
+ private final Iterator listenersSnapshot;
+ private final ListenerInvocation listenerInvocation;
+
+ private ListenerNotificationTask(ListenerInvocation listenerInvocation) {
+ this.listenersSnapshot = listeners.iterator();
+ this.listenerInvocation = listenerInvocation;
+ }
+
+ public void execute() {
+ while (listenersSnapshot.hasNext()) {
+ listenersSnapshot.next().invoke(listenerInvocation);
}
}
}
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 4939e62a2b..b84f1c1f2b 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
@@ -15,24 +15,66 @@
*/
package com.google.android.exoplayer2.ext.cast;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
+import android.util.SparseArray;
import android.util.SparseIntArray;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Timeline;
-import com.google.android.gms.cast.MediaInfo;
-import com.google.android.gms.cast.MediaQueueItem;
import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
/**
* A {@link Timeline} for Cast media queues.
*/
/* package */ final class CastTimeline extends Timeline {
+ /** Holds {@link Timeline} related data for a Cast media item. */
+ public static final class ItemData {
+
+ /** Holds no media information. */
+ public static final ItemData EMPTY = new ItemData();
+
+ /** The duration of the item in microseconds, or {@link C#TIME_UNSET} if unknown. */
+ public final long durationUs;
+ /**
+ * The default start position of the item in microseconds, or {@link C#TIME_UNSET} if unknown.
+ */
+ public final long defaultPositionUs;
+
+ private ItemData() {
+ this(/* durationUs= */ C.TIME_UNSET, /* defaultPositionUs */ C.TIME_UNSET);
+ }
+
+ /**
+ * Creates an instance.
+ *
+ * @param durationUs See {@link #durationsUs}.
+ * @param defaultPositionUs See {@link #defaultPositionUs}.
+ */
+ public ItemData(long durationUs, long defaultPositionUs) {
+ this.durationUs = durationUs;
+ this.defaultPositionUs = defaultPositionUs;
+ }
+
+ /** Returns an instance with the given {@link #durationsUs}. */
+ public ItemData copyWithDurationUs(long durationUs) {
+ if (durationUs == this.durationUs) {
+ return this;
+ }
+ return new ItemData(durationUs, defaultPositionUs);
+ }
+
+ /** Returns an instance with the given {@link #defaultPositionsUs}. */
+ public ItemData copyWithDefaultPositionUs(long defaultPositionUs) {
+ if (defaultPositionUs == this.defaultPositionUs) {
+ return this;
+ }
+ return new ItemData(durationUs, defaultPositionUs);
+ }
+ }
+
+ /** {@link Timeline} for a cast queue that has no items. */
public static final CastTimeline EMPTY_CAST_TIMELINE =
- new CastTimeline(Collections.emptyList(), Collections.emptyMap());
+ new CastTimeline(new int[0], new SparseArray<>());
private final SparseIntArray idsToIndex;
private final int[] ids;
@@ -40,28 +82,23 @@ import java.util.Map;
private final long[] defaultPositionsUs;
/**
- * @param items A list of cast media queue items to represent.
- * @param contentIdToDurationUsMap A map of content id to duration in microseconds.
+ * Creates a Cast timeline from the given data.
+ *
+ * @param itemIds The ids of the items in the timeline.
+ * @param itemIdToData Maps item ids to {@link ItemData}.
*/
- public CastTimeline(List items, Map contentIdToDurationUsMap) {
- int itemCount = items.size();
- int index = 0;
+ public CastTimeline(int[] itemIds, SparseArray itemIdToData) {
+ int itemCount = itemIds.length;
idsToIndex = new SparseIntArray(itemCount);
- ids = new int[itemCount];
+ ids = Arrays.copyOf(itemIds, itemCount);
durationsUs = new long[itemCount];
defaultPositionsUs = new long[itemCount];
- for (MediaQueueItem item : items) {
- int itemId = item.getItemId();
- ids[index] = itemId;
- idsToIndex.put(itemId, index);
- MediaInfo mediaInfo = item.getMedia();
- String contentId = mediaInfo.getContentId();
- durationsUs[index] =
- contentIdToDurationUsMap.containsKey(contentId)
- ? contentIdToDurationUsMap.get(contentId)
- : CastUtils.getStreamDurationUs(mediaInfo);
- defaultPositionsUs[index] = (long) (item.getStartTime() * C.MICROS_PER_SECOND);
- index++;
+ for (int i = 0; i < ids.length; i++) {
+ int id = ids[i];
+ idsToIndex.put(id, i);
+ ItemData data = itemIdToData.get(id, ItemData.EMPTY);
+ durationsUs[i] = data.durationUs;
+ defaultPositionsUs[i] = data.defaultPositionUs;
}
}
@@ -80,6 +117,7 @@ import java.util.Map;
Object tag = setTag ? ids[windowIndex] : null;
return window.set(
tag,
+ /* manifest= */ null,
/* presentationStartTimeMs= */ C.TIME_UNSET,
/* windowStartTimeMs= */ C.TIME_UNSET,
/* isSeekable= */ !isDynamic,
@@ -108,7 +146,7 @@ import java.util.Map;
}
@Override
- public Object getUidOfPeriod(int periodIndex) {
+ public Integer getUidOfPeriod(int periodIndex) {
return ids[periodIndex];
}
diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimelineTracker.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimelineTracker.java
index 412bfb476d..40c93a115a 100644
--- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimelineTracker.java
+++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimelineTracker.java
@@ -15,53 +15,84 @@
*/
package com.google.android.exoplayer2.ext.cast;
-import com.google.android.gms.cast.MediaInfo;
+import android.util.SparseArray;
+import com.google.android.exoplayer2.C;
import com.google.android.gms.cast.MediaQueueItem;
import com.google.android.gms.cast.MediaStatus;
-import java.util.HashMap;
+import com.google.android.gms.cast.framework.media.RemoteMediaClient;
import java.util.HashSet;
-import java.util.List;
/**
- * Creates {@link CastTimeline}s from cast receiver app media status.
+ * Creates {@link CastTimeline CastTimelines} from cast receiver app status updates.
*
* This class keeps track of the duration reported by the current item to fill any missing
* durations in the media queue items [See internal: b/65152553].
*/
/* package */ final class CastTimelineTracker {
- private final HashMap contentIdToDurationUsMap;
- private final HashSet scratchContentIdSet;
+ private final SparseArray itemIdToData;
public CastTimelineTracker() {
- contentIdToDurationUsMap = new HashMap<>();
- scratchContentIdSet = new HashSet<>();
+ itemIdToData = new SparseArray<>();
}
/**
- * Returns a {@link CastTimeline} that represent the given {@code status}.
+ * Returns a {@link CastTimeline} that represents the state of the given {@code
+ * remoteMediaClient}.
*
- * @param status The Cast media status.
- * @return A {@link CastTimeline} that represent the given {@code status}.
+ * Returned timelines may contain values obtained from {@code remoteMediaClient} in previous
+ * invocations of this method.
+ *
+ * @param remoteMediaClient The Cast media client.
+ * @return A {@link CastTimeline} that represents the given {@code remoteMediaClient} status.
*/
- public CastTimeline getCastTimeline(MediaStatus status) {
- MediaInfo mediaInfo = status.getMediaInfo();
- List items = status.getQueueItems();
- removeUnusedDurationEntries(items);
-
- if (mediaInfo != null) {
- String contentId = mediaInfo.getContentId();
- long durationUs = CastUtils.getStreamDurationUs(mediaInfo);
- contentIdToDurationUsMap.put(contentId, durationUs);
+ public CastTimeline getCastTimeline(RemoteMediaClient remoteMediaClient) {
+ int[] itemIds = remoteMediaClient.getMediaQueue().getItemIds();
+ if (itemIds.length > 0) {
+ // Only remove unused items when there is something in the queue to avoid removing all entries
+ // if the remote media client clears the queue temporarily. See [Internal ref: b/128825216].
+ removeUnusedItemDataEntries(itemIds);
}
- return new CastTimeline(items, contentIdToDurationUsMap);
+
+ // TODO: Reset state when the app instance changes [Internal ref: b/129672468].
+ MediaStatus mediaStatus = remoteMediaClient.getMediaStatus();
+ if (mediaStatus == null) {
+ return CastTimeline.EMPTY_CAST_TIMELINE;
+ }
+
+ int currentItemId = mediaStatus.getCurrentItemId();
+ long durationUs = CastUtils.getStreamDurationUs(mediaStatus.getMediaInfo());
+ itemIdToData.put(
+ currentItemId,
+ itemIdToData
+ .get(currentItemId, CastTimeline.ItemData.EMPTY)
+ .copyWithDurationUs(durationUs));
+
+ for (MediaQueueItem item : mediaStatus.getQueueItems()) {
+ int itemId = item.getItemId();
+ itemIdToData.put(
+ itemId,
+ itemIdToData
+ .get(itemId, CastTimeline.ItemData.EMPTY)
+ .copyWithDefaultPositionUs((long) (item.getStartTime() * C.MICROS_PER_SECOND)));
+ }
+
+ return new CastTimeline(itemIds, itemIdToData);
}
- private void removeUnusedDurationEntries(List items) {
- scratchContentIdSet.clear();
- for (MediaQueueItem item : items) {
- scratchContentIdSet.add(item.getMedia().getContentId());
+ private void removeUnusedItemDataEntries(int[] itemIds) {
+ HashSet scratchItemIds = new HashSet<>(/* initialCapacity= */ itemIds.length * 2);
+ for (int id : itemIds) {
+ scratchItemIds.add(id);
+ }
+
+ int index = 0;
+ while (index < itemIdToData.size()) {
+ if (!scratchItemIds.contains(itemIdToData.keyAt(index))) {
+ itemIdToData.removeAt(index);
+ } else {
+ index++;
+ }
}
- contentIdToDurationUsMap.keySet().retainAll(scratchContentIdSet);
}
}
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 997857f6b5..1dc25576a0 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
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.ext.cast;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.gms.cast.CastStatusCodes;
@@ -31,11 +32,13 @@ import com.google.android.gms.cast.MediaTrack;
* unknown or not applicable.
*
* @param mediaInfo The media info to get the duration from.
- * @return The duration in microseconds.
+ * @return The duration in microseconds, or {@link C#TIME_UNSET} if unknown or not applicable.
*/
- public static long getStreamDurationUs(MediaInfo mediaInfo) {
- long durationMs =
- mediaInfo != null ? mediaInfo.getStreamDuration() : MediaInfo.UNKNOWN_DURATION;
+ public static long getStreamDurationUs(@Nullable MediaInfo mediaInfo) {
+ if (mediaInfo == null) {
+ return C.TIME_UNSET;
+ }
+ long durationMs = mediaInfo.getStreamDuration();
return durationMs != MediaInfo.UNKNOWN_DURATION ? C.msToUs(durationMs) : C.TIME_UNSET;
}
@@ -109,6 +112,7 @@ import com.google.android.gms.cast.MediaTrack;
/* codecs= */ null,
/* bitrate= */ Format.NO_VALUE,
/* selectionFlags= */ 0,
+ /* roleFlags= */ 0,
mediaTrack.getLanguage());
}
diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java
index 06f0bec971..ebadb0a08a 100644
--- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java
+++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java
@@ -20,6 +20,7 @@ import com.google.android.gms.cast.CastMediaControlIntent;
import com.google.android.gms.cast.framework.CastOptions;
import com.google.android.gms.cast.framework.OptionsProvider;
import com.google.android.gms.cast.framework.SessionProvider;
+import java.util.Collections;
import java.util.List;
/**
@@ -27,16 +28,38 @@ import java.util.List;
*/
public final class DefaultCastOptionsProvider implements OptionsProvider {
+ /**
+ * App id of the Default Media Receiver app. Apps that do not require DRM support may use this
+ * receiver receiver app ID.
+ *
+ * See https://developers.google.com/cast/docs/caf_receiver/#default_media_receiver.
+ */
+ public static final String APP_ID_DEFAULT_RECEIVER =
+ CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID;
+
+ /**
+ * App id for receiver app with rudimentary support for DRM.
+ *
+ *
This app id is only suitable for ExoPlayer's Cast Demo app, and it is not intended for
+ * production use. In order to use DRM, custom receiver apps should be used. For environments that
+ * do not require DRM, the default receiver app should be used (see {@link
+ * #APP_ID_DEFAULT_RECEIVER}).
+ */
+ // TODO: Add a documentation resource link for DRM support in the receiver app [Internal ref:
+ // b/128603245].
+ public static final String APP_ID_DEFAULT_RECEIVER_WITH_DRM = "A12D4273";
+
@Override
public CastOptions getCastOptions(Context context) {
return new CastOptions.Builder()
- .setReceiverApplicationId(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID)
- .setStopReceiverApplicationWhenEndingSession(true).build();
+ .setReceiverApplicationId(APP_ID_DEFAULT_RECEIVER_WITH_DRM)
+ .setStopReceiverApplicationWhenEndingSession(true)
+ .build();
}
@Override
public List getAdditionalSessionProviders(Context context) {
- return null;
+ return Collections.emptyList();
}
}
diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultMediaItemConverter.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultMediaItemConverter.java
new file mode 100644
index 0000000000..098803a512
--- /dev/null
+++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultMediaItemConverter.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2019 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.cast;
+
+import android.net.Uri;
+import androidx.annotation.Nullable;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ext.cast.MediaItem.DrmConfiguration;
+import com.google.android.gms.cast.MediaInfo;
+import com.google.android.gms.cast.MediaMetadata;
+import com.google.android.gms.cast.MediaQueueItem;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.UUID;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/** Default {@link MediaItemConverter} implementation. */
+public final class DefaultMediaItemConverter implements MediaItemConverter {
+
+ private static final String KEY_MEDIA_ITEM = "mediaItem";
+ private static final String KEY_PLAYER_CONFIG = "exoPlayerConfig";
+ private static final String KEY_URI = "uri";
+ private static final String KEY_TITLE = "title";
+ private static final String KEY_MIME_TYPE = "mimeType";
+ private static final String KEY_DRM_CONFIGURATION = "drmConfiguration";
+ private static final String KEY_UUID = "uuid";
+ private static final String KEY_LICENSE_URI = "licenseUri";
+ private static final String KEY_REQUEST_HEADERS = "requestHeaders";
+
+ @Override
+ public MediaItem toMediaItem(MediaQueueItem item) {
+ return getMediaItem(item.getMedia().getCustomData());
+ }
+
+ @Override
+ public MediaQueueItem toMediaQueueItem(MediaItem item) {
+ if (item.mimeType == null) {
+ throw new IllegalArgumentException("The item must specify its mimeType");
+ }
+ MediaMetadata metadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);
+ if (item.title != null) {
+ metadata.putString(MediaMetadata.KEY_TITLE, item.title);
+ }
+ MediaInfo mediaInfo =
+ new MediaInfo.Builder(item.uri.toString())
+ .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
+ .setContentType(item.mimeType)
+ .setMetadata(metadata)
+ .setCustomData(getCustomData(item))
+ .build();
+ return new MediaQueueItem.Builder(mediaInfo).build();
+ }
+
+ // Deserialization.
+
+ private static MediaItem getMediaItem(JSONObject customData) {
+ try {
+ JSONObject mediaItemJson = customData.getJSONObject(KEY_MEDIA_ITEM);
+ MediaItem.Builder builder = new MediaItem.Builder();
+ builder.setUri(Uri.parse(mediaItemJson.getString(KEY_URI)));
+ if (mediaItemJson.has(KEY_TITLE)) {
+ builder.setTitle(mediaItemJson.getString(KEY_TITLE));
+ }
+ if (mediaItemJson.has(KEY_MIME_TYPE)) {
+ builder.setMimeType(mediaItemJson.getString(KEY_MIME_TYPE));
+ }
+ if (mediaItemJson.has(KEY_DRM_CONFIGURATION)) {
+ builder.setDrmConfiguration(
+ getDrmConfiguration(mediaItemJson.getJSONObject(KEY_DRM_CONFIGURATION)));
+ }
+ return builder.build();
+ } catch (JSONException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static DrmConfiguration getDrmConfiguration(JSONObject json) throws JSONException {
+ UUID uuid = UUID.fromString(json.getString(KEY_UUID));
+ Uri licenseUri = Uri.parse(json.getString(KEY_LICENSE_URI));
+ JSONObject requestHeadersJson = json.getJSONObject(KEY_REQUEST_HEADERS);
+ HashMap requestHeaders = new HashMap<>();
+ for (Iterator iterator = requestHeadersJson.keys(); iterator.hasNext(); ) {
+ String key = iterator.next();
+ requestHeaders.put(key, requestHeadersJson.getString(key));
+ }
+ return new DrmConfiguration(uuid, licenseUri, requestHeaders);
+ }
+
+ // Serialization.
+
+ private static JSONObject getCustomData(MediaItem item) {
+ JSONObject json = new JSONObject();
+ try {
+ json.put(KEY_MEDIA_ITEM, getMediaItemJson(item));
+ JSONObject playerConfigJson = getPlayerConfigJson(item);
+ if (playerConfigJson != null) {
+ json.put(KEY_PLAYER_CONFIG, playerConfigJson);
+ }
+ } catch (JSONException e) {
+ throw new RuntimeException(e);
+ }
+ return json;
+ }
+
+ private static JSONObject getMediaItemJson(MediaItem item) throws JSONException {
+ JSONObject json = new JSONObject();
+ json.put(KEY_URI, item.uri.toString());
+ json.put(KEY_TITLE, item.title);
+ json.put(KEY_MIME_TYPE, item.mimeType);
+ if (item.drmConfiguration != null) {
+ json.put(KEY_DRM_CONFIGURATION, getDrmConfigurationJson(item.drmConfiguration));
+ }
+ return json;
+ }
+
+ private static JSONObject getDrmConfigurationJson(DrmConfiguration drmConfiguration)
+ throws JSONException {
+ JSONObject json = new JSONObject();
+ json.put(KEY_UUID, drmConfiguration.uuid);
+ json.put(KEY_LICENSE_URI, drmConfiguration.licenseUri);
+ json.put(KEY_REQUEST_HEADERS, new JSONObject(drmConfiguration.requestHeaders));
+ return json;
+ }
+
+ @Nullable
+ private static JSONObject getPlayerConfigJson(MediaItem item) throws JSONException {
+ DrmConfiguration drmConfiguration = item.drmConfiguration;
+ if (drmConfiguration == null) {
+ return null;
+ }
+
+ String drmScheme;
+ if (C.WIDEVINE_UUID.equals(drmConfiguration.uuid)) {
+ drmScheme = "widevine";
+ } else if (C.PLAYREADY_UUID.equals(drmConfiguration.uuid)) {
+ drmScheme = "playready";
+ } else {
+ return null;
+ }
+
+ JSONObject exoPlayerConfigJson = new JSONObject();
+ exoPlayerConfigJson.put("withCredentials", false);
+ exoPlayerConfigJson.put("protectionSystem", drmScheme);
+ if (drmConfiguration.licenseUri != null) {
+ exoPlayerConfigJson.put("licenseUrl", drmConfiguration.licenseUri);
+ }
+ if (!drmConfiguration.requestHeaders.isEmpty()) {
+ exoPlayerConfigJson.put("headers", new JSONObject(drmConfiguration.requestHeaders));
+ }
+
+ return exoPlayerConfigJson;
+ }
+}
diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItem.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItem.java
new file mode 100644
index 0000000000..7ac0da7078
--- /dev/null
+++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItem.java
@@ -0,0 +1,175 @@
+/*
+ * 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.cast;
+
+import android.net.Uri;
+import androidx.annotation.Nullable;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+import java.util.Collections;
+import java.util.Map;
+import java.util.UUID;
+
+/** Representation of a media item. */
+public final class MediaItem {
+
+ /** A builder for {@link MediaItem} instances. */
+ public static final class Builder {
+
+ @Nullable private Uri uri;
+ @Nullable private String title;
+ @Nullable private String mimeType;
+ @Nullable private DrmConfiguration drmConfiguration;
+
+ /** See {@link MediaItem#uri}. */
+ public Builder setUri(String uri) {
+ return setUri(Uri.parse(uri));
+ }
+
+ /** See {@link MediaItem#uri}. */
+ public Builder setUri(Uri uri) {
+ this.uri = uri;
+ return this;
+ }
+
+ /** See {@link MediaItem#title}. */
+ public Builder setTitle(String title) {
+ this.title = title;
+ return this;
+ }
+
+ /** See {@link MediaItem#mimeType}. */
+ public Builder setMimeType(String mimeType) {
+ this.mimeType = mimeType;
+ return this;
+ }
+
+ /** See {@link MediaItem#drmConfiguration}. */
+ public Builder setDrmConfiguration(DrmConfiguration drmConfiguration) {
+ this.drmConfiguration = drmConfiguration;
+ return this;
+ }
+
+ /** Returns a new {@link MediaItem} instance with the current builder values. */
+ public MediaItem build() {
+ Assertions.checkNotNull(uri);
+ return new MediaItem(uri, title, mimeType, drmConfiguration);
+ }
+ }
+
+ /** DRM configuration for a media item. */
+ public static final class DrmConfiguration {
+
+ /** The UUID of the protection scheme. */
+ public final UUID uuid;
+
+ /**
+ * Optional license server {@link Uri}. If {@code null} then the license server must be
+ * specified by the media.
+ */
+ @Nullable public final Uri licenseUri;
+
+ /** Headers that should be attached to any license requests. */
+ public final Map requestHeaders;
+
+ /**
+ * Creates an instance.
+ *
+ * @param uuid See {@link #uuid}.
+ * @param licenseUri See {@link #licenseUri}.
+ * @param requestHeaders See {@link #requestHeaders}.
+ */
+ public DrmConfiguration(
+ UUID uuid, @Nullable Uri licenseUri, @Nullable Map requestHeaders) {
+ this.uuid = uuid;
+ this.licenseUri = licenseUri;
+ this.requestHeaders =
+ requestHeaders == null
+ ? Collections.emptyMap()
+ : Collections.unmodifiableMap(requestHeaders);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+
+ DrmConfiguration other = (DrmConfiguration) obj;
+ return uuid.equals(other.uuid)
+ && Util.areEqual(licenseUri, other.licenseUri)
+ && requestHeaders.equals(other.requestHeaders);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = uuid.hashCode();
+ result = 31 * result + (licenseUri != null ? licenseUri.hashCode() : 0);
+ result = 31 * result + requestHeaders.hashCode();
+ return result;
+ }
+ }
+
+ /** The media {@link Uri}. */
+ public final Uri uri;
+
+ /** The title of the item, or {@code null} if unspecified. */
+ @Nullable public final String title;
+
+ /** The mime type for the media, or {@code null} if unspecified. */
+ @Nullable public final String mimeType;
+
+ /** Optional {@link DrmConfiguration} for the media. */
+ @Nullable public final DrmConfiguration drmConfiguration;
+
+ private MediaItem(
+ Uri uri,
+ @Nullable String title,
+ @Nullable String mimeType,
+ @Nullable DrmConfiguration drmConfiguration) {
+ this.uri = uri;
+ this.title = title;
+ this.mimeType = mimeType;
+ this.drmConfiguration = drmConfiguration;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ MediaItem other = (MediaItem) obj;
+ return uri.equals(other.uri)
+ && Util.areEqual(title, other.title)
+ && Util.areEqual(mimeType, other.mimeType)
+ && Util.areEqual(drmConfiguration, other.drmConfiguration);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = uri.hashCode();
+ result = 31 * result + (title == null ? 0 : title.hashCode());
+ result = 31 * result + (drmConfiguration == null ? 0 : drmConfiguration.hashCode());
+ result = 31 * result + (mimeType == null ? 0 : mimeType.hashCode());
+ return result;
+ }
+}
diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemConverter.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemConverter.java
new file mode 100644
index 0000000000..23633aa4d2
--- /dev/null
+++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemConverter.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2019 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.cast;
+
+import com.google.android.gms.cast.MediaQueueItem;
+
+/** Converts between {@link MediaItem} and the Cast SDK's {@link MediaQueueItem}. */
+public interface MediaItemConverter {
+
+ /**
+ * Converts a {@link MediaItem} to a {@link MediaQueueItem}.
+ *
+ * @param mediaItem The {@link MediaItem}.
+ * @return An equivalent {@link MediaQueueItem}.
+ */
+ MediaQueueItem toMediaQueueItem(MediaItem mediaItem);
+
+ /**
+ * Converts a {@link MediaQueueItem} to a {@link MediaItem}.
+ *
+ * @param mediaQueueItem The {@link MediaQueueItem}.
+ * @return The equivalent {@link MediaItem}.
+ */
+ MediaItem toMediaItem(MediaQueueItem mediaQueueItem);
+}
diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/SessionAvailabilityListener.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/SessionAvailabilityListener.java
new file mode 100644
index 0000000000..c686c496c6
--- /dev/null
+++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/SessionAvailabilityListener.java
@@ -0,0 +1,26 @@
+/*
+ * 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.cast;
+
+/** Listener of changes in the cast session availability. */
+public interface SessionAvailabilityListener {
+
+ /** Called when a cast session becomes available to the player. */
+ void onCastSessionAvailable();
+
+ /** Called when the cast session becomes unavailable. */
+ void onCastSessionUnavailable();
+}
diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/package-info.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/package-info.java
new file mode 100644
index 0000000000..07055905a6
--- /dev/null
+++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+@NonNullApi
+package com.google.android.exoplayer2.ext.cast;
+
+import com.google.android.exoplayer2.util.NonNullApi;
diff --git a/extensions/cast/src/test/AndroidManifest.xml b/extensions/cast/src/test/AndroidManifest.xml
index aea8bda663..35a5150a47 100644
--- a/extensions/cast/src/test/AndroidManifest.xml
+++ b/extensions/cast/src/test/AndroidManifest.xml
@@ -14,4 +14,6 @@
limitations under the License.
-->
-
+
+
+
diff --git a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastTimelineTrackerTest.java b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastTimelineTrackerTest.java
index 4c60e7c0b3..69b25e4456 100644
--- a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastTimelineTrackerTest.java
+++ b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastTimelineTrackerTest.java
@@ -15,23 +15,23 @@
*/
package com.google.android.exoplayer2.ext.cast;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.TimelineAsserts;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.gms.cast.MediaInfo;
-import com.google.android.gms.cast.MediaQueueItem;
import com.google.android.gms.cast.MediaStatus;
-import java.util.ArrayList;
+import com.google.android.gms.cast.framework.media.MediaQueue;
+import com.google.android.gms.cast.framework.media.RemoteMediaClient;
+import java.util.Collections;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
-import org.robolectric.RobolectricTestRunner;
/** Tests for {@link CastTimelineTracker}. */
-@RunWith(RobolectricTestRunner.class)
+@RunWith(AndroidJUnit4.class)
public class CastTimelineTrackerTest {
- private static final long DURATION_1_MS = 1000;
private static final long DURATION_2_MS = 2000;
private static final long DURATION_3_MS = 3000;
private static final long DURATION_4_MS = 4000;
@@ -39,91 +39,89 @@ public class CastTimelineTrackerTest {
/** Tests that duration of the current media info is correctly propagated to the timeline. */
@Test
- public void testGetCastTimeline() {
- MediaInfo mediaInfo;
- MediaStatus status =
- mockMediaStatus(
- new int[] {1, 2, 3},
- new String[] {"contentId1", "contentId2", "contentId3"},
- new long[] {DURATION_1_MS, MediaInfo.UNKNOWN_DURATION, MediaInfo.UNKNOWN_DURATION});
-
+ public void testGetCastTimelinePersistsDuration() {
CastTimelineTracker tracker = new CastTimelineTracker();
- mediaInfo = getMediaInfo("contentId1", DURATION_1_MS);
- Mockito.when(status.getMediaInfo()).thenReturn(mediaInfo);
- TimelineAsserts.assertPeriodDurations(
- tracker.getCastTimeline(status), C.msToUs(DURATION_1_MS), C.TIME_UNSET, C.TIME_UNSET);
- mediaInfo = getMediaInfo("contentId3", DURATION_3_MS);
- Mockito.when(status.getMediaInfo()).thenReturn(mediaInfo);
+ RemoteMediaClient remoteMediaClient =
+ mockRemoteMediaClient(
+ /* itemIds= */ new int[] {1, 2, 3, 4, 5},
+ /* currentItemId= */ 2,
+ /* currentDurationMs= */ DURATION_2_MS);
TimelineAsserts.assertPeriodDurations(
- tracker.getCastTimeline(status),
- C.msToUs(DURATION_1_MS),
+ tracker.getCastTimeline(remoteMediaClient),
C.TIME_UNSET,
- C.msToUs(DURATION_3_MS));
+ C.msToUs(DURATION_2_MS),
+ C.TIME_UNSET,
+ C.TIME_UNSET,
+ C.TIME_UNSET);
- mediaInfo = getMediaInfo("contentId2", DURATION_2_MS);
- Mockito.when(status.getMediaInfo()).thenReturn(mediaInfo);
+ remoteMediaClient =
+ mockRemoteMediaClient(
+ /* itemIds= */ new int[] {1, 2, 3},
+ /* currentItemId= */ 3,
+ /* currentDurationMs= */ DURATION_3_MS);
TimelineAsserts.assertPeriodDurations(
- tracker.getCastTimeline(status),
- C.msToUs(DURATION_1_MS),
+ tracker.getCastTimeline(remoteMediaClient),
+ C.TIME_UNSET,
C.msToUs(DURATION_2_MS),
C.msToUs(DURATION_3_MS));
- MediaStatus newStatus =
- mockMediaStatus(
- new int[] {4, 1, 5, 3},
- new String[] {"contentId4", "contentId1", "contentId5", "contentId3"},
- new long[] {
- MediaInfo.UNKNOWN_DURATION,
- MediaInfo.UNKNOWN_DURATION,
- DURATION_5_MS,
- MediaInfo.UNKNOWN_DURATION
- });
- mediaInfo = getMediaInfo("contentId5", DURATION_5_MS);
- Mockito.when(newStatus.getMediaInfo()).thenReturn(mediaInfo);
+ remoteMediaClient =
+ mockRemoteMediaClient(
+ /* itemIds= */ new int[] {1, 3},
+ /* currentItemId= */ 3,
+ /* currentDurationMs= */ DURATION_3_MS);
TimelineAsserts.assertPeriodDurations(
- tracker.getCastTimeline(newStatus),
- C.TIME_UNSET,
- C.msToUs(DURATION_1_MS),
- C.msToUs(DURATION_5_MS),
- C.msToUs(DURATION_3_MS));
+ tracker.getCastTimeline(remoteMediaClient), C.TIME_UNSET, C.msToUs(DURATION_3_MS));
- mediaInfo = getMediaInfo("contentId3", DURATION_3_MS);
- Mockito.when(newStatus.getMediaInfo()).thenReturn(mediaInfo);
+ remoteMediaClient =
+ mockRemoteMediaClient(
+ /* itemIds= */ new int[] {1, 2, 3, 4, 5},
+ /* currentItemId= */ 4,
+ /* currentDurationMs= */ DURATION_4_MS);
TimelineAsserts.assertPeriodDurations(
- tracker.getCastTimeline(newStatus),
+ tracker.getCastTimeline(remoteMediaClient),
C.TIME_UNSET,
- C.msToUs(DURATION_1_MS),
- C.msToUs(DURATION_5_MS),
- C.msToUs(DURATION_3_MS));
-
- mediaInfo = getMediaInfo("contentId4", DURATION_4_MS);
- Mockito.when(newStatus.getMediaInfo()).thenReturn(mediaInfo);
- TimelineAsserts.assertPeriodDurations(
- tracker.getCastTimeline(newStatus),
+ C.TIME_UNSET,
+ C.msToUs(DURATION_3_MS),
C.msToUs(DURATION_4_MS),
- C.msToUs(DURATION_1_MS),
- C.msToUs(DURATION_5_MS),
- C.msToUs(DURATION_3_MS));
+ C.TIME_UNSET);
+
+ remoteMediaClient =
+ mockRemoteMediaClient(
+ /* itemIds= */ new int[] {1, 2, 3, 4, 5},
+ /* currentItemId= */ 5,
+ /* currentDurationMs= */ DURATION_5_MS);
+ TimelineAsserts.assertPeriodDurations(
+ tracker.getCastTimeline(remoteMediaClient),
+ C.TIME_UNSET,
+ C.TIME_UNSET,
+ C.msToUs(DURATION_3_MS),
+ C.msToUs(DURATION_4_MS),
+ C.msToUs(DURATION_5_MS));
}
- private static MediaStatus mockMediaStatus(
- int[] itemIds, String[] contentIds, long[] durationsMs) {
- ArrayList items = new ArrayList<>();
- for (int i = 0; i < contentIds.length; i++) {
- MediaInfo mediaInfo = getMediaInfo(contentIds[i], durationsMs[i]);
- MediaQueueItem item = Mockito.mock(MediaQueueItem.class);
- Mockito.when(item.getMedia()).thenReturn(mediaInfo);
- Mockito.when(item.getItemId()).thenReturn(itemIds[i]);
- items.add(item);
- }
+ private static RemoteMediaClient mockRemoteMediaClient(
+ int[] itemIds, int currentItemId, long currentDurationMs) {
+ RemoteMediaClient remoteMediaClient = Mockito.mock(RemoteMediaClient.class);
MediaStatus status = Mockito.mock(MediaStatus.class);
- Mockito.when(status.getQueueItems()).thenReturn(items);
- return status;
+ Mockito.when(status.getQueueItems()).thenReturn(Collections.emptyList());
+ Mockito.when(remoteMediaClient.getMediaStatus()).thenReturn(status);
+ Mockito.when(status.getMediaInfo()).thenReturn(getMediaInfo(currentDurationMs));
+ Mockito.when(status.getCurrentItemId()).thenReturn(currentItemId);
+ MediaQueue mediaQueue = mockMediaQueue(itemIds);
+ Mockito.when(remoteMediaClient.getMediaQueue()).thenReturn(mediaQueue);
+ return remoteMediaClient;
}
- private static MediaInfo getMediaInfo(String contentId, long durationMs) {
- return new MediaInfo.Builder(contentId)
+ private static MediaQueue mockMediaQueue(int[] itemIds) {
+ MediaQueue mediaQueue = Mockito.mock(MediaQueue.class);
+ Mockito.when(mediaQueue.getItemIds()).thenReturn(itemIds);
+ return mediaQueue;
+ }
+
+ private static MediaInfo getMediaInfo(long durationMs) {
+ return new MediaInfo.Builder(/*contentId= */ "")
.setStreamDuration(durationMs)
.setContentType(MimeTypes.APPLICATION_MP4)
.setStreamType(MediaInfo.STREAM_TYPE_NONE)
diff --git a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/DefaultMediaItemConverterTest.java b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/DefaultMediaItemConverterTest.java
new file mode 100644
index 0000000000..cf9b9d3496
--- /dev/null
+++ b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/DefaultMediaItemConverterTest.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2019 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.cast;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.net.Uri;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ext.cast.MediaItem.DrmConfiguration;
+import com.google.android.gms.cast.MediaQueueItem;
+import java.util.Collections;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Test for {@link DefaultMediaItemConverter}. */
+@RunWith(AndroidJUnit4.class)
+public class DefaultMediaItemConverterTest {
+
+ @Test
+ public void serialize_deserialize_minimal() {
+ MediaItem.Builder builder = new MediaItem.Builder();
+ MediaItem item = builder.setUri(Uri.parse("http://example.com")).setMimeType("mime").build();
+
+ DefaultMediaItemConverter converter = new DefaultMediaItemConverter();
+ MediaQueueItem queueItem = converter.toMediaQueueItem(item);
+ MediaItem reconstructedItem = converter.toMediaItem(queueItem);
+
+ assertThat(reconstructedItem).isEqualTo(item);
+ }
+
+ @Test
+ public void serialize_deserialize_complete() {
+ MediaItem.Builder builder = new MediaItem.Builder();
+ MediaItem item =
+ builder
+ .setUri(Uri.parse("http://example.com"))
+ .setTitle("title")
+ .setMimeType("mime")
+ .setDrmConfiguration(
+ new DrmConfiguration(
+ C.WIDEVINE_UUID,
+ Uri.parse("http://license.com"),
+ Collections.singletonMap("key", "value")))
+ .build();
+
+ DefaultMediaItemConverter converter = new DefaultMediaItemConverter();
+ MediaQueueItem queueItem = converter.toMediaQueueItem(item);
+ MediaItem reconstructedItem = converter.toMediaItem(queueItem);
+
+ assertThat(reconstructedItem).isEqualTo(item);
+ }
+}
diff --git a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/MediaItemTest.java b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/MediaItemTest.java
new file mode 100644
index 0000000000..7b410a8fbc
--- /dev/null
+++ b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/MediaItemTest.java
@@ -0,0 +1,86 @@
+/*
+ * 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.cast;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.net.Uri;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.util.MimeTypes;
+import java.util.HashMap;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Test for {@link MediaItem}. */
+@RunWith(AndroidJUnit4.class)
+public class MediaItemTest {
+
+ @Test
+ public void buildMediaItem_doesNotChangeState() {
+ MediaItem.Builder builder = new MediaItem.Builder();
+ MediaItem item1 =
+ builder
+ .setUri(Uri.parse("http://example.com"))
+ .setTitle("title")
+ .setMimeType(MimeTypes.AUDIO_MP4)
+ .build();
+ MediaItem item2 = builder.build();
+ assertThat(item1).isEqualTo(item2);
+ }
+
+ @Test
+ public void equals_withEqualDrmSchemes_returnsTrue() {
+ MediaItem.Builder builder1 = new MediaItem.Builder();
+ MediaItem mediaItem1 =
+ builder1
+ .setUri(Uri.parse("www.google.com"))
+ .setDrmConfiguration(buildDrmConfiguration(1))
+ .build();
+ MediaItem.Builder builder2 = new MediaItem.Builder();
+ MediaItem mediaItem2 =
+ builder2
+ .setUri(Uri.parse("www.google.com"))
+ .setDrmConfiguration(buildDrmConfiguration(1))
+ .build();
+ assertThat(mediaItem1).isEqualTo(mediaItem2);
+ }
+
+ @Test
+ public void equals_withDifferentDrmRequestHeaders_returnsFalse() {
+ MediaItem.Builder builder1 = new MediaItem.Builder();
+ MediaItem mediaItem1 =
+ builder1
+ .setUri(Uri.parse("www.google.com"))
+ .setDrmConfiguration(buildDrmConfiguration(1))
+ .build();
+ MediaItem.Builder builder2 = new MediaItem.Builder();
+ MediaItem mediaItem2 =
+ builder2
+ .setUri(Uri.parse("www.google.com"))
+ .setDrmConfiguration(buildDrmConfiguration(2))
+ .build();
+ assertThat(mediaItem1).isNotEqualTo(mediaItem2);
+ }
+
+ private static MediaItem.DrmConfiguration buildDrmConfiguration(int seed) {
+ HashMap requestHeaders = new HashMap<>();
+ requestHeaders.put("key1", "value1");
+ requestHeaders.put("key2", "value2" + seed);
+ return new MediaItem.DrmConfiguration(
+ C.WIDEVINE_UUID, Uri.parse("www.uri1.com"), requestHeaders);
+ }
+}
diff --git a/extensions/cast/src/test/resources/robolectric.properties b/extensions/cast/src/test/resources/robolectric.properties
deleted file mode 100644
index 2f3210368e..0000000000
--- a/extensions/cast/src/test/resources/robolectric.properties
+++ /dev/null
@@ -1 +0,0 @@
-manifest=src/test/AndroidManifest.xml
diff --git a/extensions/cronet/README.md b/extensions/cronet/README.md
index f1f6d68c81..dc64b862b6 100644
--- a/extensions/cronet/README.md
+++ b/extensions/cronet/README.md
@@ -2,7 +2,7 @@
The Cronet extension is an [HttpDataSource][] implementation using [Cronet][].
-[HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/upstream/HttpDataSource.html
+[HttpDataSource]: https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/upstream/HttpDataSource.html
[Cronet]: https://chromium.googlesource.com/chromium/src/+/master/components/cronet?autodive=0%2F%2F
## Getting the extension ##
@@ -52,4 +52,4 @@ respectively.
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.cronet.*`
belong to this module.
-[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
+[Javadoc]: https://exoplayer.dev/doc/reference/index.html
diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle
index 7d8c217b58..9c49ba94e1 100644
--- a/extensions/cronet/build.gradle
+++ b/extensions/cronet/build.gradle
@@ -16,10 +16,9 @@ apply plugin: 'com.android.library'
android {
compileSdkVersion project.ext.compileSdkVersion
- buildToolsVersion project.ext.buildToolsVersion
defaultConfig {
- minSdkVersion 16
+ minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
}
@@ -27,14 +26,18 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
+
+ testOptions.unitTests.includeAndroidResources = true
}
dependencies {
- api 'org.chromium.net:cronet-embedded:66.3359.158'
+ api 'org.chromium.net:cronet-embedded:75.3770.101'
implementation project(modulePrefix + 'library-core')
- implementation 'com.android.support:support-annotations:' + supportLibraryVersion
+ implementation 'androidx.annotation:annotation:1.1.0'
+ compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
testImplementation project(modulePrefix + 'library')
- testImplementation project(modulePrefix + 'testutils-robolectric')
+ testImplementation project(modulePrefix + 'testutils')
+ testImplementation 'org.robolectric:robolectric:' + robolectricVersion
}
ext {
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 5c6f5dafd9..ed92523017 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
@@ -15,11 +15,14 @@
*/
package com.google.android.exoplayer2.ext.cronet;
+import static com.google.android.exoplayer2.util.Util.castNonNull;
+
import android.net.Uri;
+import androidx.annotation.Nullable;
import android.text.TextUtils;
-import android.util.Log;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
+import com.google.android.exoplayer2.metadata.icy.IcyHeaders;
import com.google.android.exoplayer2.upstream.BaseDataSource;
import com.google.android.exoplayer2.upstream.DataSourceException;
import com.google.android.exoplayer2.upstream.DataSpec;
@@ -27,6 +30,7 @@ import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.ConditionVariable;
+import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Predicate;
import java.io.IOException;
import java.net.SocketTimeoutException;
@@ -39,6 +43,7 @@ import java.util.Map.Entry;
import java.util.concurrent.Executor;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
import org.chromium.net.CronetEngine;
import org.chromium.net.CronetException;
import org.chromium.net.NetworkException;
@@ -111,16 +116,17 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
private final CronetEngine cronetEngine;
private final Executor executor;
- private final Predicate contentTypePredicate;
private final int connectTimeoutMs;
private final int readTimeoutMs;
private final boolean resetTimeoutOnRedirects;
private final boolean handleSetCookieRequests;
- private final RequestProperties defaultRequestProperties;
+ @Nullable private final RequestProperties defaultRequestProperties;
private final RequestProperties requestProperties;
private final ConditionVariable operation;
private final Clock clock;
+ @Nullable private Predicate contentTypePredicate;
+
// Accessed by the calling thread only.
private boolean opened;
private long bytesToSkip;
@@ -128,18 +134,18 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
// Written from the calling thread only. currentUrlRequest.start() calls ensure writes are visible
// to reads made by the Cronet thread.
- private UrlRequest currentUrlRequest;
- private DataSpec currentDataSpec;
+ @Nullable private UrlRequest currentUrlRequest;
+ @Nullable private DataSpec currentDataSpec;
// Reference written and read by calling thread only. Passed to Cronet thread as a local variable.
// operation.open() calls ensure writes into the buffer are visible to reads made by the calling
// thread.
- private ByteBuffer readBuffer;
+ @Nullable private ByteBuffer readBuffer;
// Written from the Cronet thread only. operation.open() calls ensure writes are visible to reads
// made by the calling thread.
- private UrlResponseInfo responseInfo;
- private IOException exception;
+ @Nullable private UrlResponseInfo responseInfo;
+ @Nullable private IOException exception;
private boolean finished;
private volatile long currentConnectTimeoutMs;
@@ -151,21 +157,15 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
* hop from Cronet's internal network thread to the response handling thread. However, to
* avoid slowing down overall network performance, care must be taken to make sure response
* handling is a fast operation when using a direct executor.
- * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
- * predicate then an {@link InvalidContentTypeException} is thrown from {@link
- * #open(DataSpec)}.
*/
- public CronetDataSource(
- CronetEngine cronetEngine, Executor executor, Predicate contentTypePredicate) {
+ public CronetDataSource(CronetEngine cronetEngine, Executor executor) {
this(
cronetEngine,
executor,
- contentTypePredicate,
DEFAULT_CONNECT_TIMEOUT_MILLIS,
DEFAULT_READ_TIMEOUT_MILLIS,
- false,
- null,
- false);
+ /* resetTimeoutOnRedirects= */ false,
+ /* defaultRequestProperties= */ null);
}
/**
@@ -175,32 +175,28 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
* hop from Cronet's internal network thread to the response handling thread. However, to
* avoid slowing down overall network performance, care must be taken to make sure response
* handling is a fast operation when using a direct executor.
- * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
- * predicate then an {@link InvalidContentTypeException} is thrown from {@link
- * #open(DataSpec)}.
* @param connectTimeoutMs The connection timeout, in milliseconds.
* @param readTimeoutMs The read timeout, in milliseconds.
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
- * @param defaultRequestProperties The default request properties to be used.
+ * @param defaultRequestProperties Optional default {@link RequestProperties} to be sent to the
+ * server as HTTP headers on every request.
*/
public CronetDataSource(
CronetEngine cronetEngine,
Executor executor,
- Predicate contentTypePredicate,
int connectTimeoutMs,
int readTimeoutMs,
boolean resetTimeoutOnRedirects,
- RequestProperties defaultRequestProperties) {
+ @Nullable RequestProperties defaultRequestProperties) {
this(
cronetEngine,
executor,
- contentTypePredicate,
connectTimeoutMs,
readTimeoutMs,
resetTimeoutOnRedirects,
Clock.DEFAULT,
defaultRequestProperties,
- false);
+ /* handleSetCookieRequests= */ false);
}
/**
@@ -210,29 +206,25 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
* hop from Cronet's internal network thread to the response handling thread. However, to
* avoid slowing down overall network performance, care must be taken to make sure response
* handling is a fast operation when using a direct executor.
- * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
- * predicate then an {@link InvalidContentTypeException} is thrown from {@link
- * #open(DataSpec)}.
* @param connectTimeoutMs The connection timeout, in milliseconds.
* @param readTimeoutMs The read timeout, in milliseconds.
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
- * @param defaultRequestProperties The default request properties to be used.
+ * @param defaultRequestProperties Optional default {@link RequestProperties} to be sent to the
+ * server as HTTP headers on every request.
* @param handleSetCookieRequests Whether "Set-Cookie" requests on redirect should be forwarded to
* the redirect url in the "Cookie" header.
*/
public CronetDataSource(
CronetEngine cronetEngine,
Executor executor,
- Predicate contentTypePredicate,
int connectTimeoutMs,
int readTimeoutMs,
boolean resetTimeoutOnRedirects,
- RequestProperties defaultRequestProperties,
+ @Nullable RequestProperties defaultRequestProperties,
boolean handleSetCookieRequests) {
this(
cronetEngine,
executor,
- contentTypePredicate,
connectTimeoutMs,
readTimeoutMs,
resetTimeoutOnRedirects,
@@ -241,21 +233,127 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
handleSetCookieRequests);
}
+ /**
+ * @param cronetEngine A CronetEngine.
+ * @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may
+ * be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a thread
+ * hop from Cronet's internal network thread to the response handling thread. However, to
+ * avoid slowing down overall network performance, care must be taken to make sure response
+ * handling is a fast operation when using a direct executor.
+ * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
+ * predicate then an {@link InvalidContentTypeException} is thrown from {@link
+ * #open(DataSpec)}.
+ * @deprecated Use {@link #CronetDataSource(CronetEngine, Executor)} and {@link
+ * #setContentTypePredicate(Predicate)}.
+ */
+ @Deprecated
+ public CronetDataSource(
+ CronetEngine cronetEngine,
+ Executor executor,
+ @Nullable Predicate contentTypePredicate) {
+ this(
+ cronetEngine,
+ executor,
+ contentTypePredicate,
+ DEFAULT_CONNECT_TIMEOUT_MILLIS,
+ DEFAULT_READ_TIMEOUT_MILLIS,
+ /* resetTimeoutOnRedirects= */ false,
+ /* defaultRequestProperties= */ null);
+ }
+
+ /**
+ * @param cronetEngine A CronetEngine.
+ * @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may
+ * be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a thread
+ * hop from Cronet's internal network thread to the response handling thread. However, to
+ * avoid slowing down overall network performance, care must be taken to make sure response
+ * handling is a fast operation when using a direct executor.
+ * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
+ * predicate then an {@link InvalidContentTypeException} is thrown from {@link
+ * #open(DataSpec)}.
+ * @param connectTimeoutMs The connection timeout, in milliseconds.
+ * @param readTimeoutMs The read timeout, in milliseconds.
+ * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
+ * @param defaultRequestProperties Optional default {@link RequestProperties} to be sent to the
+ * server as HTTP headers on every request.
+ * @deprecated Use {@link #CronetDataSource(CronetEngine, Executor, int, int, boolean,
+ * RequestProperties)} and {@link #setContentTypePredicate(Predicate)}.
+ */
+ @Deprecated
+ public CronetDataSource(
+ CronetEngine cronetEngine,
+ Executor executor,
+ @Nullable Predicate contentTypePredicate,
+ int connectTimeoutMs,
+ int readTimeoutMs,
+ boolean resetTimeoutOnRedirects,
+ @Nullable RequestProperties defaultRequestProperties) {
+ this(
+ cronetEngine,
+ executor,
+ contentTypePredicate,
+ connectTimeoutMs,
+ readTimeoutMs,
+ resetTimeoutOnRedirects,
+ defaultRequestProperties,
+ /* handleSetCookieRequests= */ false);
+ }
+
+ /**
+ * @param cronetEngine A CronetEngine.
+ * @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may
+ * be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a thread
+ * hop from Cronet's internal network thread to the response handling thread. However, to
+ * avoid slowing down overall network performance, care must be taken to make sure response
+ * handling is a fast operation when using a direct executor.
+ * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
+ * predicate then an {@link InvalidContentTypeException} is thrown from {@link
+ * #open(DataSpec)}.
+ * @param connectTimeoutMs The connection timeout, in milliseconds.
+ * @param readTimeoutMs The read timeout, in milliseconds.
+ * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
+ * @param defaultRequestProperties Optional default {@link RequestProperties} to be sent to the
+ * server as HTTP headers on every request.
+ * @param handleSetCookieRequests Whether "Set-Cookie" requests on redirect should be forwarded to
+ * the redirect url in the "Cookie" header.
+ * @deprecated Use {@link #CronetDataSource(CronetEngine, Executor, int, int, boolean,
+ * RequestProperties, boolean)} and {@link #setContentTypePredicate(Predicate)}.
+ */
+ @Deprecated
+ public CronetDataSource(
+ CronetEngine cronetEngine,
+ Executor executor,
+ @Nullable Predicate contentTypePredicate,
+ int connectTimeoutMs,
+ int readTimeoutMs,
+ boolean resetTimeoutOnRedirects,
+ @Nullable RequestProperties defaultRequestProperties,
+ boolean handleSetCookieRequests) {
+ this(
+ cronetEngine,
+ executor,
+ connectTimeoutMs,
+ readTimeoutMs,
+ resetTimeoutOnRedirects,
+ Clock.DEFAULT,
+ defaultRequestProperties,
+ handleSetCookieRequests);
+ this.contentTypePredicate = contentTypePredicate;
+ }
+
/* package */ CronetDataSource(
CronetEngine cronetEngine,
Executor executor,
- Predicate contentTypePredicate,
int connectTimeoutMs,
int readTimeoutMs,
boolean resetTimeoutOnRedirects,
Clock clock,
- RequestProperties defaultRequestProperties,
+ @Nullable RequestProperties defaultRequestProperties,
boolean handleSetCookieRequests) {
super(/* isNetwork= */ true);
this.urlRequestCallback = new UrlRequestCallback();
this.cronetEngine = Assertions.checkNotNull(cronetEngine);
this.executor = Assertions.checkNotNull(executor);
- this.contentTypePredicate = contentTypePredicate;
this.connectTimeoutMs = connectTimeoutMs;
this.readTimeoutMs = readTimeoutMs;
this.resetTimeoutOnRedirects = resetTimeoutOnRedirects;
@@ -266,6 +364,17 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
operation = new ConditionVariable();
}
+ /**
+ * Sets a content type {@link Predicate}. If a content type is rejected by the predicate then a
+ * {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link #open(DataSpec)}.
+ *
+ * @param contentTypePredicate The content type {@link Predicate}, or {@code null} to clear a
+ * predicate that was previously set.
+ */
+ public void setContentTypePredicate(@Nullable Predicate contentTypePredicate) {
+ this.contentTypePredicate = contentTypePredicate;
+ }
+
// HttpDataSource implementation.
@Override
@@ -289,6 +398,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
}
@Override
+ @Nullable
public Uri getUri() {
return responseInfo == null ? null : Uri.parse(responseInfo.getUrl());
}
@@ -301,22 +411,23 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
operation.close();
resetConnectTimeout();
currentDataSpec = dataSpec;
+ UrlRequest urlRequest;
try {
- currentUrlRequest = buildRequestBuilder(dataSpec).build();
+ urlRequest = buildRequestBuilder(dataSpec).build();
+ currentUrlRequest = urlRequest;
} catch (IOException e) {
- throw new OpenException(e, currentDataSpec, Status.IDLE);
+ throw new OpenException(e, dataSpec, Status.IDLE);
}
- currentUrlRequest.start();
+ urlRequest.start();
transferInitializing(dataSpec);
try {
boolean connectionOpened = blockUntilConnectTimeout();
if (exception != null) {
- throw new OpenException(exception, currentDataSpec, getStatus(currentUrlRequest));
+ throw new OpenException(exception, dataSpec, getStatus(urlRequest));
} else if (!connectionOpened) {
// The timeout was reached before the connection was opened.
- throw new OpenException(
- new SocketTimeoutException(), dataSpec, getStatus(currentUrlRequest));
+ throw new OpenException(new SocketTimeoutException(), dataSpec, getStatus(urlRequest));
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
@@ -324,10 +435,15 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
}
// Check for a valid response code.
+ UrlResponseInfo responseInfo = Assertions.checkNotNull(this.responseInfo);
int responseCode = responseInfo.getHttpStatusCode();
if (responseCode < 200 || responseCode > 299) {
- InvalidResponseCodeException exception = new InvalidResponseCodeException(responseCode,
- responseInfo.getAllHeaders(), currentDataSpec);
+ InvalidResponseCodeException exception =
+ new InvalidResponseCodeException(
+ responseCode,
+ responseInfo.getHttpStatusText(),
+ responseInfo.getAllHeaders(),
+ dataSpec);
if (responseCode == 416) {
exception.initCause(new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE));
}
@@ -335,11 +451,12 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
}
// Check for a valid content type.
+ Predicate contentTypePredicate = this.contentTypePredicate;
if (contentTypePredicate != null) {
List contentTypeHeaders = responseInfo.getAllHeaders().get(CONTENT_TYPE);
String contentType = isEmpty(contentTypeHeaders) ? null : contentTypeHeaders.get(0);
- if (!contentTypePredicate.evaluate(contentType)) {
- throw new InvalidContentTypeException(contentType, currentDataSpec);
+ if (contentType != null && !contentTypePredicate.evaluate(contentType)) {
+ throw new InvalidContentTypeException(contentType, dataSpec);
}
}
@@ -349,7 +466,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0;
// Calculate the content length.
- if (!getIsCompressed(responseInfo)) {
+ if (!isCompressed(responseInfo)) {
if (dataSpec.length != C.LENGTH_UNSET) {
bytesRemaining = dataSpec.length;
} else {
@@ -358,7 +475,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
} else {
// If the response is compressed then the content length will be that of the compressed data
// which isn't what we want. Always use the dataSpec length in this case.
- bytesRemaining = currentDataSpec.length;
+ bytesRemaining = dataSpec.length;
}
opened = true;
@@ -377,37 +494,19 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
return C.RESULT_END_OF_INPUT;
}
+ ByteBuffer readBuffer = this.readBuffer;
if (readBuffer == null) {
readBuffer = ByteBuffer.allocateDirect(READ_BUFFER_SIZE_BYTES);
readBuffer.limit(0);
+ this.readBuffer = readBuffer;
}
while (!readBuffer.hasRemaining()) {
// Fill readBuffer with more data from Cronet.
operation.close();
readBuffer.clear();
- currentUrlRequest.read(readBuffer);
- try {
- if (!operation.block(readTimeoutMs)) {
- throw new SocketTimeoutException();
- }
- } 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(
- 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);
- }
+ readInternal(castNonNull(readBuffer));
- if (exception != null) {
- throw new HttpDataSourceException(exception, currentDataSpec,
- HttpDataSourceException.TYPE_READ);
- } else if (finished) {
+ if (finished) {
bytesRemaining = 0;
return C.RESULT_END_OF_INPUT;
} else {
@@ -432,6 +531,115 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
return bytesRead;
}
+ /**
+ * Reads up to {@code buffer.remaining()} bytes of data and stores them into {@code buffer},
+ * starting at {@code buffer.position()}. Advances the position of the buffer by the number of
+ * bytes read and returns this length.
+ *
+ * If there is an error, a {@link HttpDataSourceException} is thrown and the contents of {@code
+ * buffer} should be ignored. If the exception has error code {@code
+ * HttpDataSourceException.TYPE_READ}, note that Cronet may continue writing into {@code buffer}
+ * after the method has returned. Thus the caller should not attempt to reuse the buffer.
+ *
+ *
If {@code buffer.remaining()} is zero then 0 is returned. Otherwise, if no data is available
+ * because the end of the opened range has been reached, then {@link C#RESULT_END_OF_INPUT} is
+ * returned. Otherwise, the call will block until at least one byte of data has been read and the
+ * number of bytes read is returned.
+ *
+ *
Passed buffer must be direct ByteBuffer. If you have a non-direct ByteBuffer, consider the
+ * alternative read method with its backed array.
+ *
+ * @param buffer The ByteBuffer into which the read data should be stored. Must be a direct
+ * ByteBuffer.
+ * @return The number of bytes read, or {@link C#RESULT_END_OF_INPUT} if no data is available
+ * because the end of the opened range has been reached.
+ * @throws HttpDataSourceException If an error occurs reading from the source.
+ * @throws IllegalArgumentException If {@code buffer} is not a direct ByteBuffer.
+ */
+ public int read(ByteBuffer buffer) throws HttpDataSourceException {
+ Assertions.checkState(opened);
+
+ if (!buffer.isDirect()) {
+ throw new IllegalArgumentException("Passed buffer is not a direct ByteBuffer");
+ }
+ if (!buffer.hasRemaining()) {
+ return 0;
+ } else if (bytesRemaining == 0) {
+ return C.RESULT_END_OF_INPUT;
+ }
+ int readLength = buffer.remaining();
+
+ if (readBuffer != null) {
+ // Skip all the bytes we can from readBuffer if there are still bytes to skip.
+ if (bytesToSkip != 0) {
+ if (bytesToSkip >= readBuffer.remaining()) {
+ bytesToSkip -= readBuffer.remaining();
+ readBuffer.position(readBuffer.limit());
+ } else {
+ readBuffer.position(readBuffer.position() + (int) bytesToSkip);
+ bytesToSkip = 0;
+ }
+ }
+
+ // If there is existing data in the readBuffer, read as much as possible. Return if any read.
+ int copyBytes = copyByteBuffer(/* src= */ readBuffer, /* dst= */ buffer);
+ if (copyBytes != 0) {
+ if (bytesRemaining != C.LENGTH_UNSET) {
+ bytesRemaining -= copyBytes;
+ }
+ bytesTransferred(copyBytes);
+ return copyBytes;
+ }
+ }
+
+ boolean readMore = true;
+ while (readMore) {
+ // If bytesToSkip > 0, read into intermediate buffer that we can discard instead of caller's
+ // buffer. If we do not need to skip bytes, we may write to buffer directly.
+ final boolean useCallerBuffer = bytesToSkip == 0;
+
+ operation.close();
+
+ if (!useCallerBuffer) {
+ if (readBuffer == null) {
+ readBuffer = ByteBuffer.allocateDirect(READ_BUFFER_SIZE_BYTES);
+ } else {
+ readBuffer.clear();
+ }
+ if (bytesToSkip < READ_BUFFER_SIZE_BYTES) {
+ readBuffer.limit((int) bytesToSkip);
+ }
+ }
+
+ // Fill buffer with more data from Cronet.
+ readInternal(useCallerBuffer ? buffer : castNonNull(readBuffer));
+
+ if (finished) {
+ bytesRemaining = 0;
+ return C.RESULT_END_OF_INPUT;
+ } else {
+ // The operation didn't time out, fail or finish, and therefore data must have been read.
+ Assertions.checkState(
+ useCallerBuffer
+ ? readLength > buffer.remaining()
+ : castNonNull(readBuffer).position() > 0);
+ // If we meant to skip bytes, subtract what was left and repeat, otherwise, continue.
+ if (useCallerBuffer) {
+ readMore = false;
+ } else {
+ bytesToSkip -= castNonNull(readBuffer).position();
+ }
+ }
+ }
+
+ final int bytesRead = readLength - buffer.remaining();
+ if (bytesRemaining != C.LENGTH_UNSET) {
+ bytesRemaining -= bytesRead;
+ }
+ bytesTransferred(bytesRead);
+ return bytesRead;
+ }
+
@Override
public synchronized void close() {
if (currentUrlRequest != null) {
@@ -451,6 +659,18 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
}
}
+ /** Returns current {@link UrlRequest}. May be null if the data source is not opened. */
+ @Nullable
+ protected UrlRequest getCurrentUrlRequest() {
+ return currentUrlRequest;
+ }
+
+ /** Returns current {@link UrlResponseInfo}. May be null if the data source is not opened. */
+ @Nullable
+ protected UrlResponseInfo getCurrentUrlResponseInfo() {
+ return responseInfo;
+ }
+
// Internal methods.
private UrlRequest.Builder buildRequestBuilder(DataSpec dataSpec) throws IOException {
@@ -476,6 +696,11 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
if (dataSpec.httpBody != null && !isContentTypeHeaderSet) {
throw new IOException("HTTP request with non-empty body must set Content-Type");
}
+ if (dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_ICY_METADATA)) {
+ requestBuilder.addHeader(
+ IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_NAME,
+ IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_VALUE);
+ }
// Set the Range header.
if (dataSpec.position != 0 || dataSpec.length != C.LENGTH_UNSET) {
StringBuilder rangeValue = new StringBuilder();
@@ -487,7 +712,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
}
requestBuilder.addHeader("Range", rangeValue.toString());
}
- // TODO: Uncomment when https://bugs.chromium.org/p/chromium/issues/detail?id=767025 is fixed
+ // TODO: Uncomment when https://bugs.chromium.org/p/chromium/issues/detail?id=711810 is fixed
// (adjusting the code as necessary).
// Force identity encoding unless gzip is allowed.
// if (!dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP)) {
@@ -516,7 +741,49 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
currentConnectTimeoutMs = clock.elapsedRealtime() + connectTimeoutMs;
}
- private static boolean getIsCompressed(UrlResponseInfo info) {
+ /**
+ * Reads up to {@code buffer.remaining()} bytes of data from {@code currentUrlRequest} and stores
+ * them into {@code buffer}. If there is an error and {@code buffer == readBuffer}, then it resets
+ * the current {@code readBuffer} object so that it is not reused in the future.
+ *
+ * @param buffer The ByteBuffer into which the read data is stored. Must be a direct ByteBuffer.
+ * @throws HttpDataSourceException If an error occurs reading from the source.
+ */
+ @SuppressWarnings("ReferenceEquality")
+ private void readInternal(ByteBuffer buffer) throws HttpDataSourceException {
+ castNonNull(currentUrlRequest).read(buffer);
+ try {
+ if (!operation.block(readTimeoutMs)) {
+ throw new SocketTimeoutException();
+ }
+ } catch (InterruptedException e) {
+ // The operation is ongoing so replace buffer to avoid it being written to by this
+ // operation during a subsequent request.
+ if (buffer == readBuffer) {
+ readBuffer = null;
+ }
+ Thread.currentThread().interrupt();
+ throw new HttpDataSourceException(
+ new InterruptedIOException(e),
+ castNonNull(currentDataSpec),
+ HttpDataSourceException.TYPE_READ);
+ } catch (SocketTimeoutException e) {
+ // The operation is ongoing so replace buffer to avoid it being written to by this
+ // operation during a subsequent request.
+ if (buffer == readBuffer) {
+ readBuffer = null;
+ }
+ throw new HttpDataSourceException(
+ e, castNonNull(currentDataSpec), HttpDataSourceException.TYPE_READ);
+ }
+
+ if (exception != null) {
+ throw new HttpDataSourceException(
+ exception, castNonNull(currentDataSpec), HttpDataSourceException.TYPE_READ);
+ }
+ }
+
+ private static boolean isCompressed(UrlResponseInfo info) {
for (Map.Entry entry : info.getAllHeadersAsList()) {
if (entry.getKey().equalsIgnoreCase("Content-Encoding")) {
return !entry.getValue().equalsIgnoreCase("identity");
@@ -594,10 +861,22 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
return statusHolder[0];
}
- private static boolean isEmpty(List> list) {
+ @EnsuresNonNullIf(result = false, expression = "#1")
+ private static boolean isEmpty(@Nullable List> list) {
return list == null || list.isEmpty();
}
+ // Copy as much as possible from the src buffer into dst buffer.
+ // Returns the number of bytes copied.
+ private static int copyByteBuffer(ByteBuffer src, ByteBuffer dst) {
+ int remaining = Math.min(src.remaining(), dst.remaining());
+ int limit = src.limit();
+ src.limit(src.position() + remaining);
+ dst.put(src);
+ src.limit(limit);
+ return remaining;
+ }
+
private final class UrlRequestCallback extends UrlRequest.Callback {
@Override
@@ -606,12 +885,15 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
if (request != currentUrlRequest) {
return;
}
- if (currentDataSpec.httpMethod == DataSpec.HTTP_METHOD_POST) {
+ UrlRequest urlRequest = Assertions.checkNotNull(currentUrlRequest);
+ DataSpec dataSpec = Assertions.checkNotNull(currentDataSpec);
+ if (dataSpec.httpMethod == DataSpec.HTTP_METHOD_POST) {
int responseCode = info.getHttpStatusCode();
// The industry standard is to disregard POST redirects when the status code is 307 or 308.
if (responseCode == 307 || responseCode == 308) {
exception =
- new InvalidResponseCodeException(responseCode, info.getAllHeaders(), currentDataSpec);
+ new InvalidResponseCodeException(
+ responseCode, info.getHttpStatusText(), info.getAllHeaders(), dataSpec);
operation.open();
return;
}
@@ -620,40 +902,46 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
resetConnectTimeout();
}
- Map> headers = info.getAllHeaders();
- if (!handleSetCookieRequests || isEmpty(headers.get(SET_COOKIE))) {
+ if (!handleSetCookieRequests) {
request.followRedirect();
- } else {
- currentUrlRequest.cancel();
- DataSpec redirectUrlDataSpec;
- if (currentDataSpec.httpMethod == DataSpec.HTTP_METHOD_POST) {
- // For POST redirects that aren't 307 or 308, the redirect is followed but request is
- // transformed into a GET.
- redirectUrlDataSpec =
- new DataSpec(
- Uri.parse(newLocationUrl),
- DataSpec.HTTP_METHOD_GET,
- /* httpBody= */ null,
- currentDataSpec.absoluteStreamPosition,
- currentDataSpec.position,
- currentDataSpec.length,
- currentDataSpec.key,
- currentDataSpec.flags);
- } else {
- redirectUrlDataSpec = currentDataSpec.withUri(Uri.parse(newLocationUrl));
- }
- UrlRequest.Builder requestBuilder;
- try {
- requestBuilder = buildRequestBuilder(redirectUrlDataSpec);
- } catch (IOException e) {
- exception = e;
- return;
- }
- String cookieHeadersValue = parseCookies(headers.get(SET_COOKIE));
- attachCookies(requestBuilder, cookieHeadersValue);
- currentUrlRequest = requestBuilder.build();
- currentUrlRequest.start();
+ return;
}
+
+ List setCookieHeaders = info.getAllHeaders().get(SET_COOKIE);
+ if (isEmpty(setCookieHeaders)) {
+ request.followRedirect();
+ return;
+ }
+
+ urlRequest.cancel();
+ DataSpec redirectUrlDataSpec;
+ if (dataSpec.httpMethod == DataSpec.HTTP_METHOD_POST) {
+ // For POST redirects that aren't 307 or 308, the redirect is followed but request is
+ // transformed into a GET.
+ redirectUrlDataSpec =
+ new DataSpec(
+ Uri.parse(newLocationUrl),
+ DataSpec.HTTP_METHOD_GET,
+ /* httpBody= */ null,
+ dataSpec.absoluteStreamPosition,
+ dataSpec.position,
+ dataSpec.length,
+ dataSpec.key,
+ dataSpec.flags);
+ } else {
+ redirectUrlDataSpec = dataSpec.withUri(Uri.parse(newLocationUrl));
+ }
+ UrlRequest.Builder requestBuilder;
+ try {
+ requestBuilder = buildRequestBuilder(redirectUrlDataSpec);
+ } catch (IOException e) {
+ exception = e;
+ return;
+ }
+ String cookieHeadersValue = parseCookies(setCookieHeaders);
+ attachCookies(requestBuilder, cookieHeadersValue);
+ currentUrlRequest = requestBuilder.build();
+ currentUrlRequest.start();
}
@Override
diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java
index d832e4625d..4086011b4f 100644
--- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java
+++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java
@@ -15,14 +15,12 @@
*/
package com.google.android.exoplayer2.ext.cronet;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;
import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
-import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidContentTypeException;
import com.google.android.exoplayer2.upstream.TransferListener;
-import com.google.android.exoplayer2.util.Predicate;
import java.util.concurrent.Executor;
import org.chromium.net.CronetEngine;
@@ -45,8 +43,7 @@ public final class CronetDataSourceFactory extends BaseFactory {
private final CronetEngineWrapper cronetEngineWrapper;
private final Executor executor;
- private final Predicate contentTypePredicate;
- private final @Nullable TransferListener transferListener;
+ @Nullable private final TransferListener transferListener;
private final int connectTimeoutMs;
private final int readTimeoutMs;
private final boolean resetTimeoutOnRedirects;
@@ -64,21 +61,16 @@ public final class CronetDataSourceFactory extends BaseFactory {
*
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
- * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
- * predicate then an {@link InvalidContentTypeException} is thrown from {@link
- * CronetDataSource#open}.
* @param fallbackFactory A {@link HttpDataSource.Factory} which is used as a fallback in case no
* suitable CronetEngine can be build.
*/
public CronetDataSourceFactory(
CronetEngineWrapper cronetEngineWrapper,
Executor executor,
- Predicate contentTypePredicate,
HttpDataSource.Factory fallbackFactory) {
this(
cronetEngineWrapper,
executor,
- contentTypePredicate,
/* transferListener= */ null,
DEFAULT_CONNECT_TIMEOUT_MILLIS,
DEFAULT_READ_TIMEOUT_MILLIS,
@@ -98,20 +90,15 @@ public final class CronetDataSourceFactory extends BaseFactory {
*
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
- * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
- * predicate then an {@link InvalidContentTypeException} is thrown from {@link
- * CronetDataSource#open}.
* @param userAgent A user agent used to create a fallback HttpDataSource if needed.
*/
public CronetDataSourceFactory(
CronetEngineWrapper cronetEngineWrapper,
Executor executor,
- Predicate contentTypePredicate,
String userAgent) {
this(
cronetEngineWrapper,
executor,
- contentTypePredicate,
/* transferListener= */ null,
DEFAULT_CONNECT_TIMEOUT_MILLIS,
DEFAULT_READ_TIMEOUT_MILLIS,
@@ -132,9 +119,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
*
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
- * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
- * predicate then an {@link InvalidContentTypeException} is thrown from {@link
- * CronetDataSource#open}.
* @param connectTimeoutMs The connection timeout, in milliseconds.
* @param readTimeoutMs The read timeout, in milliseconds.
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
@@ -143,7 +127,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
public CronetDataSourceFactory(
CronetEngineWrapper cronetEngineWrapper,
Executor executor,
- Predicate contentTypePredicate,
int connectTimeoutMs,
int readTimeoutMs,
boolean resetTimeoutOnRedirects,
@@ -151,7 +134,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
this(
cronetEngineWrapper,
executor,
- contentTypePredicate,
/* transferListener= */ null,
DEFAULT_CONNECT_TIMEOUT_MILLIS,
DEFAULT_READ_TIMEOUT_MILLIS,
@@ -172,9 +154,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
*
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
- * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
- * predicate then an {@link InvalidContentTypeException} is thrown from {@link
- * CronetDataSource#open}.
* @param connectTimeoutMs The connection timeout, in milliseconds.
* @param readTimeoutMs The read timeout, in milliseconds.
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
@@ -184,7 +163,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
public CronetDataSourceFactory(
CronetEngineWrapper cronetEngineWrapper,
Executor executor,
- Predicate contentTypePredicate,
int connectTimeoutMs,
int readTimeoutMs,
boolean resetTimeoutOnRedirects,
@@ -192,7 +170,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
this(
cronetEngineWrapper,
executor,
- contentTypePredicate,
/* transferListener= */ null,
connectTimeoutMs,
readTimeoutMs,
@@ -212,9 +189,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
*
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
- * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
- * predicate then an {@link InvalidContentTypeException} is thrown from {@link
- * CronetDataSource#open}.
* @param transferListener An optional listener.
* @param fallbackFactory A {@link HttpDataSource.Factory} which is used as a fallback in case no
* suitable CronetEngine can be build.
@@ -222,11 +196,16 @@ public final class CronetDataSourceFactory extends BaseFactory {
public CronetDataSourceFactory(
CronetEngineWrapper cronetEngineWrapper,
Executor executor,
- Predicate contentTypePredicate,
@Nullable TransferListener transferListener,
HttpDataSource.Factory fallbackFactory) {
- this(cronetEngineWrapper, executor, contentTypePredicate, transferListener,
- DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false, fallbackFactory);
+ this(
+ cronetEngineWrapper,
+ executor,
+ transferListener,
+ DEFAULT_CONNECT_TIMEOUT_MILLIS,
+ DEFAULT_READ_TIMEOUT_MILLIS,
+ false,
+ fallbackFactory);
}
/**
@@ -241,22 +220,27 @@ public final class CronetDataSourceFactory extends BaseFactory {
*
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
- * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
- * predicate then an {@link InvalidContentTypeException} is thrown from {@link
- * CronetDataSource#open}.
* @param transferListener An optional listener.
* @param userAgent A user agent used to create a fallback HttpDataSource if needed.
*/
public CronetDataSourceFactory(
CronetEngineWrapper cronetEngineWrapper,
Executor executor,
- Predicate contentTypePredicate,
@Nullable TransferListener transferListener,
String userAgent) {
- this(cronetEngineWrapper, executor, contentTypePredicate, transferListener,
- DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false,
- new DefaultHttpDataSourceFactory(userAgent, transferListener,
- DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false));
+ this(
+ cronetEngineWrapper,
+ executor,
+ transferListener,
+ DEFAULT_CONNECT_TIMEOUT_MILLIS,
+ DEFAULT_READ_TIMEOUT_MILLIS,
+ false,
+ new DefaultHttpDataSourceFactory(
+ userAgent,
+ transferListener,
+ DEFAULT_CONNECT_TIMEOUT_MILLIS,
+ DEFAULT_READ_TIMEOUT_MILLIS,
+ false));
}
/**
@@ -267,9 +251,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
*
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
- * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
- * predicate then an {@link InvalidContentTypeException} is thrown from {@link
- * CronetDataSource#open}.
* @param transferListener An optional listener.
* @param connectTimeoutMs The connection timeout, in milliseconds.
* @param readTimeoutMs The read timeout, in milliseconds.
@@ -279,16 +260,20 @@ public final class CronetDataSourceFactory extends BaseFactory {
public CronetDataSourceFactory(
CronetEngineWrapper cronetEngineWrapper,
Executor executor,
- Predicate contentTypePredicate,
@Nullable TransferListener transferListener,
int connectTimeoutMs,
int readTimeoutMs,
boolean resetTimeoutOnRedirects,
String userAgent) {
- this(cronetEngineWrapper, executor, contentTypePredicate, transferListener,
- DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, resetTimeoutOnRedirects,
- new DefaultHttpDataSourceFactory(userAgent, transferListener, connectTimeoutMs,
- readTimeoutMs, resetTimeoutOnRedirects));
+ this(
+ cronetEngineWrapper,
+ executor,
+ transferListener,
+ DEFAULT_CONNECT_TIMEOUT_MILLIS,
+ DEFAULT_READ_TIMEOUT_MILLIS,
+ resetTimeoutOnRedirects,
+ new DefaultHttpDataSourceFactory(
+ userAgent, transferListener, connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects));
}
/**
@@ -299,9 +284,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
*
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
- * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
- * predicate then an {@link InvalidContentTypeException} is thrown from {@link
- * CronetDataSource#open}.
* @param transferListener An optional listener.
* @param connectTimeoutMs The connection timeout, in milliseconds.
* @param readTimeoutMs The read timeout, in milliseconds.
@@ -312,7 +294,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
public CronetDataSourceFactory(
CronetEngineWrapper cronetEngineWrapper,
Executor executor,
- Predicate contentTypePredicate,
@Nullable TransferListener transferListener,
int connectTimeoutMs,
int readTimeoutMs,
@@ -320,7 +301,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
HttpDataSource.Factory fallbackFactory) {
this.cronetEngineWrapper = cronetEngineWrapper;
this.executor = executor;
- this.contentTypePredicate = contentTypePredicate;
this.transferListener = transferListener;
this.connectTimeoutMs = connectTimeoutMs;
this.readTimeoutMs = readTimeoutMs;
@@ -339,7 +319,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
new CronetDataSource(
cronetEngine,
executor,
- contentTypePredicate,
connectTimeoutMs,
readTimeoutMs,
resetTimeoutOnRedirects,
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 9257411e3c..7d549be7cb 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
@@ -16,9 +16,11 @@
package com.google.android.exoplayer2.ext.cronet;
import android.content.Context;
-import android.support.annotation.IntDef;
-import android.util.Log;
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
+import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Field;
@@ -36,13 +38,14 @@ public final class CronetEngineWrapper {
private static final String TAG = "CronetEngineWrapper";
- private final CronetEngine cronetEngine;
- private final @CronetEngineSource int cronetEngineSource;
+ @Nullable private final CronetEngine cronetEngine;
+ @CronetEngineSource private final int cronetEngineSource;
/**
* Source of {@link CronetEngine}. One of {@link #SOURCE_NATIVE}, {@link #SOURCE_GMS}, {@link
* #SOURCE_UNKNOWN}, {@link #SOURCE_USER_PROVIDED} or {@link #SOURCE_UNAVAILABLE}.
*/
+ @Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({SOURCE_NATIVE, SOURCE_GMS, SOURCE_UNKNOWN, SOURCE_USER_PROVIDED, SOURCE_UNAVAILABLE})
public @interface CronetEngineSource {}
@@ -142,7 +145,8 @@ public final class CronetEngineWrapper {
*
* @return A {@link CronetEngineSource} value.
*/
- public @CronetEngineSource int getCronetEngineSource() {
+ @CronetEngineSource
+ public int getCronetEngineSource() {
return cronetEngineSource;
}
@@ -151,13 +155,14 @@ public final class CronetEngineWrapper {
*
* @return The CronetEngine, or null if no CronetEngine is available.
*/
+ @Nullable
/* package */ CronetEngine getCronetEngine() {
return cronetEngine;
}
private static class CronetProviderComparator implements Comparator {
- private final String gmsCoreCronetName;
+ @Nullable private final String gmsCoreCronetName;
private final boolean preferGMSCoreCronet;
// Multi-catch can only be used for API 19+ in this case.
diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/package-info.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/package-info.java
new file mode 100644
index 0000000000..ec0cf8df05
--- /dev/null
+++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+@NonNullApi
+package com.google.android.exoplayer2.ext.cronet;
+
+import com.google.android.exoplayer2.util.NonNullApi;
diff --git a/extensions/cronet/src/test/AndroidManifest.xml b/extensions/cronet/src/test/AndroidManifest.xml
index 82cffe17c2..d6e09107a7 100644
--- a/extensions/cronet/src/test/AndroidManifest.xml
+++ b/extensions/cronet/src/test/AndroidManifest.xml
@@ -14,4 +14,6 @@
limitations under the License.
-->
-
+
+
+
diff --git a/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java b/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java
index 117518a1eb..244ba9083b 100644
--- a/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java
+++ b/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java
@@ -19,6 +19,7 @@ import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Arrays;
@@ -28,10 +29,9 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
-import org.robolectric.RobolectricTestRunner;
/** Tests for {@link ByteArrayUploadDataProvider}. */
-@RunWith(RobolectricTestRunner.class)
+@RunWith(AndroidJUnit4.class)
public final class ByteArrayUploadDataProviderTest {
private static final byte[] TEST_DATA = new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
diff --git a/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java b/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java
index 7d47b0da64..2be369bad9 100644
--- a/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java
+++ b/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java
@@ -17,8 +17,8 @@ package com.google.android.exoplayer2.ext.cronet;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyString;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doThrow;
@@ -31,13 +31,13 @@ import static org.mockito.Mockito.when;
import android.net.Uri;
import android.os.ConditionVariable;
import android.os.SystemClock;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource.HttpDataSourceException;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Clock;
-import com.google.android.exoplayer2.util.Predicate;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.net.SocketTimeoutException;
@@ -62,10 +62,9 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
-import org.robolectric.RobolectricTestRunner;
/** Tests for {@link CronetDataSource}. */
-@RunWith(RobolectricTestRunner.class)
+@RunWith(AndroidJUnit4.class)
public final class CronetDataSourceTest {
private static final int TEST_CONNECT_TIMEOUT_MS = 100;
@@ -85,7 +84,6 @@ public final class CronetDataSourceTest {
@Mock private UrlRequest.Builder mockUrlRequestBuilder;
@Mock private UrlRequest mockUrlRequest;
- @Mock private Predicate mockContentTypePredicate;
@Mock private TransferListener mockTransferListener;
@Mock private Executor mockExecutor;
@Mock private NetworkException mockNetworkException;
@@ -95,21 +93,19 @@ public final class CronetDataSourceTest {
private boolean redirectCalled;
@Before
- public void setUp() throws Exception {
+ public void setUp() {
MockitoAnnotations.initMocks(this);
dataSourceUnderTest =
new CronetDataSource(
mockCronetEngine,
mockExecutor,
- mockContentTypePredicate,
TEST_CONNECT_TIMEOUT_MS,
TEST_READ_TIMEOUT_MS,
- true, // resetTimeoutOnRedirects
+ /* resetTimeoutOnRedirects= */ true,
Clock.DEFAULT,
- null,
- false);
+ /* defaultRequestProperties= */ null,
+ /* handleSetCookieRequests= */ false);
dataSourceUnderTest.addTransferListener(mockTransferListener);
- when(mockContentTypePredicate.evaluate(anyString())).thenReturn(true);
when(mockCronetEngine.newUrlRequestBuilder(
anyString(), any(UrlRequest.Callback.class), any(Executor.class)))
.thenReturn(mockUrlRequestBuilder);
@@ -283,7 +279,13 @@ public final class CronetDataSourceTest {
@Test
public void testRequestOpenValidatesContentTypePredicate() {
mockResponseStartSuccess();
- when(mockContentTypePredicate.evaluate(anyString())).thenReturn(false);
+
+ ArrayList testedContentTypes = new ArrayList<>();
+ dataSourceUnderTest.setContentTypePredicate(
+ (String input) -> {
+ testedContentTypes.add(input);
+ return false;
+ });
try {
dataSourceUnderTest.open(testDataSpec);
@@ -292,7 +294,8 @@ public final class CronetDataSourceTest {
assertThat(e instanceof HttpDataSource.InvalidContentTypeException).isTrue();
// Check for connection not automatically closed.
verify(mockUrlRequest, never()).cancel();
- verify(mockContentTypePredicate).evaluate(TEST_CONTENT_TYPE);
+ assertThat(testedContentTypes).hasSize(1);
+ assertThat(testedContentTypes.get(0)).isEqualTo(TEST_CONTENT_TYPE);
}
}
@@ -551,6 +554,260 @@ public final class CronetDataSourceTest {
assertThat(bytesRead).isEqualTo(16);
}
+ @Test
+ public void testRequestReadByteBufferTwice() throws HttpDataSourceException {
+ mockResponseStartSuccess();
+ mockReadSuccess(0, 16);
+
+ dataSourceUnderTest.open(testDataSpec);
+
+ ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(8);
+ int bytesRead = dataSourceUnderTest.read(returnedBuffer);
+ assertThat(bytesRead).isEqualTo(8);
+ returnedBuffer.flip();
+ assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(0, 8));
+
+ // Use a wrapped ByteBuffer instead of direct for coverage.
+ returnedBuffer.rewind();
+ bytesRead = dataSourceUnderTest.read(returnedBuffer);
+ returnedBuffer.flip();
+ assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(8, 8));
+ assertThat(bytesRead).isEqualTo(8);
+
+ // Separate cronet calls for each read.
+ verify(mockUrlRequest, times(2)).read(any(ByteBuffer.class));
+ verify(mockTransferListener, times(2))
+ .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);
+ }
+
+ @Test
+ public void testRequestIntermixRead() throws HttpDataSourceException {
+ mockResponseStartSuccess();
+ // Chunking reads into parts 6, 7, 8, 9.
+ mockReadSuccess(0, 30);
+
+ dataSourceUnderTest.open(testDataSpec);
+
+ ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(6);
+ int bytesRead = dataSourceUnderTest.read(returnedBuffer);
+ returnedBuffer.flip();
+ assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(0, 6));
+ assertThat(bytesRead).isEqualTo(6);
+
+ byte[] returnedBytes = new byte[7];
+ bytesRead += dataSourceUnderTest.read(returnedBytes, 0, 7);
+ assertThat(returnedBytes).isEqualTo(buildTestDataArray(6, 7));
+ assertThat(bytesRead).isEqualTo(6 + 7);
+
+ returnedBuffer = ByteBuffer.allocateDirect(8);
+ bytesRead += dataSourceUnderTest.read(returnedBuffer);
+ returnedBuffer.flip();
+ assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(13, 8));
+ assertThat(bytesRead).isEqualTo(6 + 7 + 8);
+
+ returnedBytes = new byte[9];
+ bytesRead += dataSourceUnderTest.read(returnedBytes, 0, 9);
+ assertThat(returnedBytes).isEqualTo(buildTestDataArray(21, 9));
+ assertThat(bytesRead).isEqualTo(6 + 7 + 8 + 9);
+
+ // First ByteBuffer call. The first byte[] call populates enough bytes for the rest.
+ verify(mockUrlRequest, times(2)).read(any(ByteBuffer.class));
+ verify(mockTransferListener, times(1))
+ .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 6);
+ verify(mockTransferListener, times(1))
+ .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 7);
+ verify(mockTransferListener, times(1))
+ .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);
+ verify(mockTransferListener, times(1))
+ .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 9);
+ }
+
+ @Test
+ public void testSecondRequestNoContentLengthReadByteBuffer() throws HttpDataSourceException {
+ mockResponseStartSuccess();
+ testResponseHeader.put("Content-Length", Long.toString(1L));
+ mockReadSuccess(0, 16);
+
+ // First request.
+ dataSourceUnderTest.open(testDataSpec);
+ ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(8);
+ dataSourceUnderTest.read(returnedBuffer);
+ dataSourceUnderTest.close();
+
+ testResponseHeader.remove("Content-Length");
+ mockReadSuccess(0, 16);
+
+ // Second request.
+ dataSourceUnderTest.open(testDataSpec);
+ returnedBuffer = ByteBuffer.allocateDirect(16);
+ returnedBuffer.limit(10);
+ int bytesRead = dataSourceUnderTest.read(returnedBuffer);
+ assertThat(bytesRead).isEqualTo(10);
+ returnedBuffer.limit(returnedBuffer.capacity());
+ bytesRead = dataSourceUnderTest.read(returnedBuffer);
+ assertThat(bytesRead).isEqualTo(6);
+ returnedBuffer.rewind();
+ bytesRead = dataSourceUnderTest.read(returnedBuffer);
+ assertThat(bytesRead).isEqualTo(C.RESULT_END_OF_INPUT);
+ }
+
+ @Test
+ public void testRangeRequestWith206ResponseReadByteBuffer() throws HttpDataSourceException {
+ mockResponseStartSuccess();
+ mockReadSuccess(1000, 5000);
+ testUrlResponseInfo = createUrlResponseInfo(206); // Server supports range requests.
+ testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
+
+ dataSourceUnderTest.open(testDataSpec);
+
+ ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(16);
+ int bytesRead = dataSourceUnderTest.read(returnedBuffer);
+ assertThat(bytesRead).isEqualTo(16);
+ returnedBuffer.flip();
+ assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(1000, 16));
+ verify(mockTransferListener)
+ .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 16);
+ }
+
+ @Test
+ public void testRangeRequestWith200ResponseReadByteBuffer() throws HttpDataSourceException {
+ // Tests for skipping bytes.
+ mockResponseStartSuccess();
+ mockReadSuccess(0, 7000);
+ testUrlResponseInfo = createUrlResponseInfo(200); // Server does not support range requests.
+ testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
+
+ dataSourceUnderTest.open(testDataSpec);
+
+ ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(16);
+ int bytesRead = dataSourceUnderTest.read(returnedBuffer);
+ assertThat(bytesRead).isEqualTo(16);
+ returnedBuffer.flip();
+ assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(1000, 16));
+ verify(mockTransferListener)
+ .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 16);
+ }
+
+ @Test
+ public void testReadByteBufferWithUnsetLength() throws HttpDataSourceException {
+ testResponseHeader.remove("Content-Length");
+ mockResponseStartSuccess();
+ mockReadSuccess(0, 16);
+
+ dataSourceUnderTest.open(testDataSpec);
+
+ ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(16);
+ returnedBuffer.limit(8);
+ int bytesRead = dataSourceUnderTest.read(returnedBuffer);
+ returnedBuffer.flip();
+ assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(0, 8));
+ assertThat(bytesRead).isEqualTo(8);
+ verify(mockTransferListener)
+ .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);
+ }
+
+ @Test
+ public void testReadByteBufferReturnsWhatItCan() throws HttpDataSourceException {
+ mockResponseStartSuccess();
+ mockReadSuccess(0, 16);
+
+ dataSourceUnderTest.open(testDataSpec);
+
+ ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(24);
+ int bytesRead = dataSourceUnderTest.read(returnedBuffer);
+ returnedBuffer.flip();
+ assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(0, 16));
+ assertThat(bytesRead).isEqualTo(16);
+ verify(mockTransferListener)
+ .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 16);
+ }
+
+ @Test
+ public void testOverreadByteBuffer() throws HttpDataSourceException {
+ testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, 16, null);
+ testResponseHeader.put("Content-Length", Long.toString(16L));
+ mockResponseStartSuccess();
+ mockReadSuccess(0, 16);
+
+ dataSourceUnderTest.open(testDataSpec);
+
+ ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(8);
+ int bytesRead = dataSourceUnderTest.read(returnedBuffer);
+ assertThat(bytesRead).isEqualTo(8);
+ returnedBuffer.flip();
+ assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(0, 8));
+
+ // The current buffer is kept if not completely consumed by DataSource reader.
+ returnedBuffer = ByteBuffer.allocateDirect(6);
+ bytesRead += dataSourceUnderTest.read(returnedBuffer);
+ assertThat(bytesRead).isEqualTo(14);
+ returnedBuffer.flip();
+ assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(8, 6));
+
+ // 2 bytes left at this point.
+ returnedBuffer = ByteBuffer.allocateDirect(8);
+ bytesRead += dataSourceUnderTest.read(returnedBuffer);
+ assertThat(bytesRead).isEqualTo(16);
+ returnedBuffer.flip();
+ assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(14, 2));
+
+ // Called on each.
+ verify(mockUrlRequest, times(3)).read(any(ByteBuffer.class));
+ verify(mockTransferListener, times(1))
+ .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);
+ verify(mockTransferListener, times(1))
+ .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 6);
+ verify(mockTransferListener, times(1))
+ .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 2);
+
+ // Now we already returned the 16 bytes initially asked.
+ // Try to read again even though all requested 16 bytes are already returned.
+ // Return C.RESULT_END_OF_INPUT
+ returnedBuffer = ByteBuffer.allocateDirect(16);
+ int bytesOverRead = dataSourceUnderTest.read(returnedBuffer);
+ assertThat(bytesOverRead).isEqualTo(C.RESULT_END_OF_INPUT);
+ assertThat(returnedBuffer.position()).isEqualTo(0);
+ // C.RESULT_END_OF_INPUT should not be reported though the TransferListener.
+ verify(mockTransferListener, never())
+ .onBytesTransferred(
+ dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, C.RESULT_END_OF_INPUT);
+ // Number of calls to cronet should not have increased.
+ verify(mockUrlRequest, times(3)).read(any(ByteBuffer.class));
+ // Check for connection not automatically closed.
+ verify(mockUrlRequest, never()).cancel();
+ assertThat(bytesRead).isEqualTo(16);
+ }
+
+ @Test
+ public void testClosedMeansClosedReadByteBuffer() throws HttpDataSourceException {
+ mockResponseStartSuccess();
+ mockReadSuccess(0, 16);
+
+ int bytesRead = 0;
+ dataSourceUnderTest.open(testDataSpec);
+
+ ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(16);
+ returnedBuffer.limit(8);
+ bytesRead += dataSourceUnderTest.read(returnedBuffer);
+ returnedBuffer.flip();
+ assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(0, 8));
+ assertThat(bytesRead).isEqualTo(8);
+
+ dataSourceUnderTest.close();
+ verify(mockTransferListener)
+ .onTransferEnd(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
+
+ try {
+ bytesRead += dataSourceUnderTest.read(returnedBuffer);
+ fail();
+ } catch (IllegalStateException e) {
+ // Expected.
+ }
+
+ // 16 bytes were attempted but only 8 should have been successfully read.
+ assertThat(bytesRead).isEqualTo(8);
+ }
+
@Test
public void testConnectTimeout() throws InterruptedException {
long startTimeMs = SystemClock.elapsedRealtime();
@@ -734,7 +991,6 @@ public final class CronetDataSourceTest {
new CronetDataSource(
mockCronetEngine,
mockExecutor,
- mockContentTypePredicate,
TEST_CONNECT_TIMEOUT_MS,
TEST_READ_TIMEOUT_MS,
true, // resetTimeoutOnRedirects
@@ -765,13 +1021,12 @@ public final class CronetDataSourceTest {
new CronetDataSource(
mockCronetEngine,
mockExecutor,
- mockContentTypePredicate,
TEST_CONNECT_TIMEOUT_MS,
TEST_READ_TIMEOUT_MS,
- true, // resetTimeoutOnRedirects
+ /* resetTimeoutOnRedirects= */ true,
Clock.DEFAULT,
- null,
- true);
+ /* defaultRequestProperties= */ null,
+ /* handleSetCookieRequests= */ true);
dataSourceUnderTest.addTransferListener(mockTransferListener);
dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
@@ -804,13 +1059,12 @@ public final class CronetDataSourceTest {
new CronetDataSource(
mockCronetEngine,
mockExecutor,
- mockContentTypePredicate,
TEST_CONNECT_TIMEOUT_MS,
TEST_READ_TIMEOUT_MS,
- true, // resetTimeoutOnRedirects
+ /* resetTimeoutOnRedirects= */ true,
Clock.DEFAULT,
- null,
- true);
+ /* defaultRequestProperties= */ null,
+ /* handleSetCookieRequests= */ true);
dataSourceUnderTest.addTransferListener(mockTransferListener);
mockSingleRedirectSuccess();
mockFollowRedirectSuccess();
@@ -855,6 +1109,36 @@ public final class CronetDataSourceTest {
}
}
+ @Test
+ public void testReadByteBufferFailure() throws HttpDataSourceException {
+ mockResponseStartSuccess();
+ mockReadFailure();
+
+ dataSourceUnderTest.open(testDataSpec);
+ ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(8);
+ try {
+ dataSourceUnderTest.read(returnedBuffer);
+ fail("dataSourceUnderTest.read() returned, but IOException expected");
+ } catch (IOException e) {
+ // Expected.
+ }
+ }
+
+ @Test
+ public void testReadNonDirectedByteBufferFailure() throws HttpDataSourceException {
+ mockResponseStartSuccess();
+ mockReadFailure();
+
+ dataSourceUnderTest.open(testDataSpec);
+ byte[] returnedBuffer = new byte[8];
+ try {
+ dataSourceUnderTest.read(ByteBuffer.wrap(returnedBuffer));
+ fail("dataSourceUnderTest.read() returned, but IllegalArgumentException expected");
+ } catch (IllegalArgumentException e) {
+ // Expected.
+ }
+ }
+
@Test
public void testReadInterrupted() throws HttpDataSourceException, InterruptedException {
mockResponseStartSuccess();
@@ -886,6 +1170,37 @@ public final class CronetDataSourceTest {
timedOutLatch.await();
}
+ @Test
+ public void testReadByteBufferInterrupted() throws HttpDataSourceException, InterruptedException {
+ mockResponseStartSuccess();
+ dataSourceUnderTest.open(testDataSpec);
+
+ final ConditionVariable startCondition = buildReadStartedCondition();
+ final CountDownLatch timedOutLatch = new CountDownLatch(1);
+ ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(8);
+ Thread thread =
+ new Thread() {
+ @Override
+ public void run() {
+ try {
+ dataSourceUnderTest.read(returnedBuffer);
+ fail();
+ } catch (HttpDataSourceException e) {
+ // Expected.
+ assertThat(e.getCause() instanceof CronetDataSource.InterruptedIOException).isTrue();
+ timedOutLatch.countDown();
+ }
+ }
+ };
+ thread.start();
+ startCondition.block();
+
+ assertNotCountedDown(timedOutLatch);
+ // Now we interrupt.
+ thread.interrupt();
+ timedOutLatch.await();
+ }
+
@Test
public void testAllowDirectExecutor() throws HttpDataSourceException {
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
@@ -1064,4 +1379,17 @@ public final class CronetDataSourceTest {
testBuffer.flip();
return testBuffer;
}
+
+ // Returns a copy of what is remaining in the src buffer from the current position to capacity.
+ private static byte[] copyByteBufferToArray(ByteBuffer src) {
+ if (src == null) {
+ return null;
+ }
+ byte[] copy = new byte[src.remaining()];
+ int index = 0;
+ while (src.hasRemaining()) {
+ copy[index++] = src.get();
+ }
+ return copy;
+ }
}
diff --git a/extensions/cronet/src/test/resources/robolectric.properties b/extensions/cronet/src/test/resources/robolectric.properties
deleted file mode 100644
index 2f3210368e..0000000000
--- a/extensions/cronet/src/test/resources/robolectric.properties
+++ /dev/null
@@ -1 +0,0 @@
-manifest=src/test/AndroidManifest.xml
diff --git a/extensions/ffmpeg/README.md b/extensions/ffmpeg/README.md
index d5a37db013..5b68f1e352 100644
--- a/extensions/ffmpeg/README.md
+++ b/extensions/ffmpeg/README.md
@@ -46,7 +46,7 @@ HOST_PLATFORM="linux-x86_64"
be supported. See the [Supported formats][] page for more details of the
available flags.
-For example, to fetch and build for armeabi-v7a,
+For example, to fetch and build FFmpeg release 4.0 for armeabi-v7a,
arm64-v8a and x86 on Linux x86_64:
```
@@ -71,7 +71,7 @@ COMMON_OPTIONS="\
" && \
cd "${FFMPEG_EXT_PATH}/jni" && \
(git -C ffmpeg pull || git clone git://source.ffmpeg.org/ffmpeg ffmpeg) && \
-cd ffmpeg && \
+cd ffmpeg && git checkout release/4.0 && \
./configure \
--libdir=android-libs/armeabi-v7a \
--arch=arm \
@@ -147,11 +147,11 @@ then implement your own logic to use the renderer for a given track.
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
[#2781]: https://github.com/google/ExoPlayer/issues/2781
-[Supported formats]: https://google.github.io/ExoPlayer/supported-formats.html#ffmpeg-extension
+[Supported formats]: https://exoplayer.dev/supported-formats.html#ffmpeg-extension
## Links ##
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.ffmpeg.*`
belong to this module.
-[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
+[Javadoc]: https://exoplayer.dev/doc/reference/index.html
diff --git a/extensions/ffmpeg/build.gradle b/extensions/ffmpeg/build.gradle
index 1630b6f775..2b5a6010a9 100644
--- a/extensions/ffmpeg/build.gradle
+++ b/extensions/ffmpeg/build.gradle
@@ -16,7 +16,6 @@ apply plugin: 'com.android.library'
android {
compileSdkVersion project.ext.compileSdkVersion
- buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@@ -33,12 +32,16 @@ android {
jniLibs.srcDir 'src/main/libs'
jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio.
}
+
+ testOptions.unitTests.includeAndroidResources = true
}
dependencies {
implementation project(modulePrefix + 'library-core')
- implementation 'com.android.support:support-annotations:' + supportLibraryVersion
+ implementation 'androidx.annotation:annotation:1.1.0'
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
+ testImplementation project(modulePrefix + 'testutils')
+ testImplementation 'org.robolectric:robolectric:' + robolectricVersion
}
ext {
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 13e3964c71..39d1ee4094 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
@@ -16,7 +16,7 @@
package com.google.android.exoplayer2.ext.ffmpeg;
import android.os.Handler;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format;
@@ -92,8 +92,8 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
}
@Override
- protected int supportsFormatInternal(DrmSessionManager drmSessionManager,
- Format format) {
+ protected int supportsFormatInternal(
+ @Nullable DrmSessionManager drmSessionManager, Format format) {
Assertions.checkNotNull(format.sampleMimeType);
if (!FfmpegLibrary.isAvailable()) {
return FORMAT_UNSUPPORTED_TYPE;
@@ -113,7 +113,7 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
}
@Override
- protected FfmpegDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto)
+ protected FfmpegDecoder createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto)
throws FfmpegDecoderException {
int initialInputBufferSize =
format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE;
@@ -145,12 +145,13 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
}
private boolean isOutputSupported(Format inputFormat) {
- return shouldUseFloatOutput(inputFormat) || supportsOutputEncoding(C.ENCODING_PCM_16BIT);
+ return shouldUseFloatOutput(inputFormat)
+ || supportsOutput(inputFormat.channelCount, C.ENCODING_PCM_16BIT);
}
private boolean shouldUseFloatOutput(Format inputFormat) {
Assertions.checkNotNull(inputFormat.sampleMimeType);
- if (!enableFloatOutput || !supportsOutputEncoding(C.ENCODING_PCM_FLOAT)) {
+ if (!enableFloatOutput || !supportsOutput(inputFormat.channelCount, C.ENCODING_PCM_FLOAT)) {
return false;
}
switch (inputFormat.sampleMimeType) {
diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java
index 6f3c623f3f..c78b02aa5b 100644
--- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java
+++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java
@@ -15,7 +15,7 @@
*/
package com.google.android.exoplayer2.ext.ffmpeg;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
@@ -37,8 +37,12 @@ import java.util.List;
private static final int OUTPUT_BUFFER_SIZE_16BIT = 65536;
private static final int OUTPUT_BUFFER_SIZE_32BIT = OUTPUT_BUFFER_SIZE_16BIT * 2;
+ // Error codes matching ffmpeg_jni.cc.
+ private static final int DECODER_ERROR_INVALID_DATA = -1;
+ private static final int DECODER_ERROR_OTHER = -2;
+
private final String codecName;
- private final @Nullable byte[] extraData;
+ @Nullable private final byte[] extraData;
private final @C.Encoding int encoding;
private final int outputBufferSize;
@@ -106,8 +110,14 @@ import java.util.List;
int inputSize = inputData.limit();
ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, outputBufferSize);
int result = ffmpegDecode(nativeContext, inputData, inputSize, outputData, outputBufferSize);
- if (result < 0) {
- return new FfmpegDecoderException("Error decoding (see logcat). Code: " + result);
+ if (result == DECODER_ERROR_INVALID_DATA) {
+ // Treat invalid data errors as non-fatal to match the behavior of MediaCodec. No output will
+ // be produced for this buffer, so mark it as decode-only to ensure that the audio sink's
+ // position is reset when more audio is produced.
+ outputBuffer.setFlags(C.BUFFER_FLAG_DECODE_ONLY);
+ return null;
+ } else if (result == DECODER_ERROR_OTHER) {
+ return new FfmpegDecoderException("Error decoding (see logcat).");
}
if (!hasOutputFormat) {
channelCount = ffmpegGetChannelCount(nativeContext);
@@ -162,28 +172,49 @@ import java.util.List;
private static @Nullable byte[] getExtraData(String mimeType, List initializationData) {
switch (mimeType) {
case MimeTypes.AUDIO_AAC:
- case MimeTypes.AUDIO_ALAC:
case MimeTypes.AUDIO_OPUS:
return initializationData.get(0);
+ case MimeTypes.AUDIO_ALAC:
+ return getAlacExtraData(initializationData);
case MimeTypes.AUDIO_VORBIS:
- byte[] header0 = initializationData.get(0);
- byte[] header1 = initializationData.get(1);
- byte[] extraData = new byte[header0.length + header1.length + 6];
- extraData[0] = (byte) (header0.length >> 8);
- extraData[1] = (byte) (header0.length & 0xFF);
- System.arraycopy(header0, 0, extraData, 2, header0.length);
- extraData[header0.length + 2] = 0;
- extraData[header0.length + 3] = 0;
- extraData[header0.length + 4] = (byte) (header1.length >> 8);
- extraData[header0.length + 5] = (byte) (header1.length & 0xFF);
- System.arraycopy(header1, 0, extraData, header0.length + 6, header1.length);
- return extraData;
+ return getVorbisExtraData(initializationData);
default:
// Other codecs do not require extra data.
return null;
}
}
+ private static byte[] getAlacExtraData(List initializationData) {
+ // FFmpeg's ALAC decoder expects an ALAC atom, which contains the ALAC "magic cookie", as extra
+ // data. initializationData[0] contains only the magic cookie, and so we need to package it into
+ // an ALAC atom. See:
+ // https://ffmpeg.org/doxygen/0.6/alac_8c.html
+ // https://github.com/macosforge/alac/blob/master/ALACMagicCookieDescription.txt
+ byte[] magicCookie = initializationData.get(0);
+ int alacAtomLength = 12 + magicCookie.length;
+ ByteBuffer alacAtom = ByteBuffer.allocate(alacAtomLength);
+ alacAtom.putInt(alacAtomLength);
+ alacAtom.putInt(0x616c6163); // type=alac
+ alacAtom.putInt(0); // version=0, flags=0
+ alacAtom.put(magicCookie, /* offset= */ 0, magicCookie.length);
+ return alacAtom.array();
+ }
+
+ private static byte[] getVorbisExtraData(List initializationData) {
+ byte[] header0 = initializationData.get(0);
+ byte[] header1 = initializationData.get(1);
+ byte[] extraData = new byte[header0.length + header1.length + 6];
+ extraData[0] = (byte) (header0.length >> 8);
+ extraData[1] = (byte) (header0.length & 0xFF);
+ System.arraycopy(header0, 0, extraData, 2, header0.length);
+ extraData[header0.length + 2] = 0;
+ extraData[header0.length + 3] = 0;
+ extraData[header0.length + 4] = (byte) (header1.length >> 8);
+ extraData[header0.length + 5] = (byte) (header1.length & 0xFF);
+ System.arraycopy(header1, 0, extraData, header0.length + 6, header1.length);
+ return extraData;
+ }
+
private native long ffmpegInitialize(
String codecName,
@Nullable byte[] extraData,
diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java
index e5018a49b3..58109c1666 100644
--- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java
+++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java
@@ -15,10 +15,11 @@
*/
package com.google.android.exoplayer2.ext.ffmpeg;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.util.LibraryLoader;
+import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.MimeTypes;
/**
@@ -30,6 +31,8 @@ public final class FfmpegLibrary {
ExoPlayerLibraryInfo.registerModule("goog.exo.ffmpeg");
}
+ private static final String TAG = "FfmpegLibrary";
+
private static final LibraryLoader LOADER =
new LibraryLoader("avutil", "avresample", "avcodec", "ffmpeg");
@@ -69,7 +72,14 @@ public final class FfmpegLibrary {
return false;
}
String codecName = getCodecName(mimeType, encoding);
- return codecName != null && ffmpegHasDecoder(codecName);
+ if (codecName == null) {
+ return false;
+ }
+ if (!ffmpegHasDecoder(codecName)) {
+ Log.w(TAG, "No " + codecName + " decoder available. Check the FFmpeg build configuration.");
+ return false;
+ }
+ return true;
}
/**
diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/package-info.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/package-info.java
new file mode 100644
index 0000000000..a9fedb19cb
--- /dev/null
+++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+@NonNullApi
+package com.google.android.exoplayer2.ext.ffmpeg;
+
+import com.google.android.exoplayer2.util.NonNullApi;
diff --git a/extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc b/extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc
index 87579ebb9a..dcd4560e4a 100644
--- a/extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc
+++ b/extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc
@@ -63,6 +63,10 @@ static const AVSampleFormat OUTPUT_FORMAT_PCM_16BIT = AV_SAMPLE_FMT_S16;
// Output format corresponding to AudioFormat.ENCODING_PCM_FLOAT.
static const AVSampleFormat OUTPUT_FORMAT_PCM_FLOAT = AV_SAMPLE_FMT_FLT;
+// Error codes matching FfmpegDecoder.java.
+static const int DECODER_ERROR_INVALID_DATA = -1;
+static const int DECODER_ERROR_OTHER = -2;
+
/**
* Returns the AVCodec with the specified name, or NULL if it is not available.
*/
@@ -79,7 +83,7 @@ AVCodecContext *createContext(JNIEnv *env, AVCodec *codec, jbyteArray extraData,
/**
* Decodes the packet into the output buffer, returning the number of bytes
- * written, or a negative value in the case of an error.
+ * written, or a negative DECODER_ERROR constant value in the case of an error.
*/
int decodePacket(AVCodecContext *context, AVPacket *packet,
uint8_t *outputBuffer, int outputSize);
@@ -238,6 +242,7 @@ AVCodecContext *createContext(JNIEnv *env, AVCodec *codec, jbyteArray extraData,
context->channels = rawChannelCount;
context->channel_layout = av_get_default_channel_layout(rawChannelCount);
}
+ context->err_recognition = AV_EF_IGNORE_ERR;
int result = avcodec_open2(context, codec, NULL);
if (result < 0) {
logError("avcodec_open2", result);
@@ -254,7 +259,8 @@ int decodePacket(AVCodecContext *context, AVPacket *packet,
result = avcodec_send_packet(context, packet);
if (result) {
logError("avcodec_send_packet", result);
- return result;
+ return result == AVERROR_INVALIDDATA ? DECODER_ERROR_INVALID_DATA
+ : DECODER_ERROR_OTHER;
}
// Dequeue output data until it runs out.
diff --git a/extensions/ffmpeg/src/test/AndroidManifest.xml b/extensions/ffmpeg/src/test/AndroidManifest.xml
new file mode 100644
index 0000000000..6ec1cea289
--- /dev/null
+++ b/extensions/ffmpeg/src/test/AndroidManifest.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
diff --git a/extensions/ffmpeg/src/test/java/com/google/android/exoplayer2/ext/ffmpeg/DefaultRenderersFactoryTest.java b/extensions/ffmpeg/src/test/java/com/google/android/exoplayer2/ext/ffmpeg/DefaultRenderersFactoryTest.java
new file mode 100644
index 0000000000..a52d1b1d7a
--- /dev/null
+++ b/extensions/ffmpeg/src/test/java/com/google/android/exoplayer2/ext/ffmpeg/DefaultRenderersFactoryTest.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2019 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.ffmpeg;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.testutil.DefaultRenderersFactoryAsserts;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit test for {@link DefaultRenderersFactoryTest} with {@link FfmpegAudioRenderer}. */
+@RunWith(AndroidJUnit4.class)
+public final class DefaultRenderersFactoryTest {
+
+ @Test
+ public void createRenderers_instantiatesVpxRenderer() {
+ DefaultRenderersFactoryAsserts.assertExtensionRendererCreated(
+ FfmpegAudioRenderer.class, C.TRACK_TYPE_AUDIO);
+ }
+}
diff --git a/extensions/flac/README.md b/extensions/flac/README.md
index fda5f0085d..78035f4d87 100644
--- a/extensions/flac/README.md
+++ b/extensions/flac/README.md
@@ -28,18 +28,19 @@ EXOPLAYER_ROOT="$(pwd)"
FLAC_EXT_PATH="${EXOPLAYER_ROOT}/extensions/flac/src/main"
```
-* Download the [Android NDK][] and set its location in an environment variable:
+* Download the [Android NDK][] (version <= 17c) and set its location in an
+ environment variable:
```
NDK_PATH=""
```
-* Download and extract flac-1.3.1 as "${FLAC_EXT_PATH}/jni/flac" folder:
+* Download and extract flac-1.3.2 as "${FLAC_EXT_PATH}/jni/flac" folder:
```
cd "${FLAC_EXT_PATH}/jni" && \
-curl https://ftp.osuosl.org/pub/xiph/releases/flac/flac-1.3.1.tar.xz | tar xJ && \
-mv flac-1.3.1 flac
+curl https://ftp.osuosl.org/pub/xiph/releases/flac/flac-1.3.2.tar.xz | tar xJ && \
+mv flac-1.3.2 flac
```
* Build the JNI native libraries from the command line:
@@ -94,4 +95,4 @@ player, then implement your own logic to use the renderer for a given track.
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.flac.*`
belong to this module.
-[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
+[Javadoc]: https://exoplayer.dev/doc/reference/index.html
diff --git a/extensions/flac/build.gradle b/extensions/flac/build.gradle
index e5261902c6..dfac2e1c26 100644
--- a/extensions/flac/build.gradle
+++ b/extensions/flac/build.gradle
@@ -16,7 +16,6 @@ apply plugin: 'com.android.library'
android {
compileSdkVersion project.ext.compileSdkVersion
- buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@@ -34,14 +33,18 @@ android {
jniLibs.srcDir 'src/main/libs'
jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio.
}
+
+ testOptions.unitTests.includeAndroidResources = true
}
dependencies {
- implementation 'com.android.support:support-annotations:' + supportLibraryVersion
implementation project(modulePrefix + 'library-core')
- androidTestImplementation 'androidx.test:runner:' + testRunnerVersion
+ implementation 'androidx.annotation:annotation:1.1.0'
+ compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
androidTestImplementation project(modulePrefix + 'testutils')
- testImplementation project(modulePrefix + 'testutils-robolectric')
+ androidTestImplementation 'androidx.test:runner:' + androidXTestVersion
+ testImplementation project(modulePrefix + 'testutils')
+ testImplementation 'org.robolectric:robolectric:' + robolectricVersion
}
ext {
diff --git a/extensions/flac/proguard-rules.txt b/extensions/flac/proguard-rules.txt
index ee0a9fa5b5..3e52f643e7 100644
--- a/extensions/flac/proguard-rules.txt
+++ b/extensions/flac/proguard-rules.txt
@@ -9,6 +9,9 @@
-keep class com.google.android.exoplayer2.ext.flac.FlacDecoderJni {
*;
}
--keep class com.google.android.exoplayer2.util.FlacStreamInfo {
+-keep class com.google.android.exoplayer2.util.FlacStreamMetadata {
+ *;
+}
+-keep class com.google.android.exoplayer2.metadata.flac.PictureFrame {
*;
}
diff --git a/extensions/flac/src/androidTest/AndroidManifest.xml b/extensions/flac/src/androidTest/AndroidManifest.xml
index cfc90117ac..39b92aa217 100644
--- a/extensions/flac/src/androidTest/AndroidManifest.xml
+++ b/extensions/flac/src/androidTest/AndroidManifest.xml
@@ -18,6 +18,9 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.google.android.exoplayer2.ext.flac.test">
+
+
+
diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeekerTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeekerTest.java
index f8e61a0609..a3770afc78 100644
--- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeekerTest.java
+++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeekerTest.java
@@ -16,22 +16,26 @@
package com.google.android.exoplayer2.ext.flac;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
-import android.test.InstrumentationTestCase;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.IOException;
+import org.junit.Before;
+import org.junit.runner.RunWith;
/** Unit test for {@link FlacBinarySearchSeeker}. */
-public final class FlacBinarySearchSeekerTest extends InstrumentationTestCase {
+@RunWith(AndroidJUnit4.class)
+public final class FlacBinarySearchSeekerTest {
private static final String NOSEEKTABLE_FLAC = "bear_no_seek.flac";
private static final int DURATION_US = 2_741_000;
- @Override
- protected void setUp() throws Exception {
- super.setUp();
+ @Before
+ public void setUp() {
if (!FlacLibrary.isAvailable()) {
fail("Flac library not available.");
}
@@ -39,7 +43,8 @@ public final class FlacBinarySearchSeekerTest extends InstrumentationTestCase {
public void testGetSeekMap_returnsSeekMapWithCorrectDuration()
throws IOException, FlacDecoderException, InterruptedException {
- byte[] data = TestUtil.getByteArray(getInstrumentation().getContext(), NOSEEKTABLE_FLAC);
+ byte[] data =
+ TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), NOSEEKTABLE_FLAC);
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
FlacDecoderJni decoderJni = new FlacDecoderJni();
@@ -47,7 +52,10 @@ public final class FlacBinarySearchSeekerTest extends InstrumentationTestCase {
FlacBinarySearchSeeker seeker =
new FlacBinarySearchSeeker(
- decoderJni.decodeMetadata(), /* firstFramePosition= */ 0, data.length, decoderJni);
+ decoderJni.decodeStreamMetadata(),
+ /* firstFramePosition= */ 0,
+ data.length,
+ decoderJni);
SeekMap seekMap = seeker.getSeekMap();
assertThat(seekMap).isNotNull();
@@ -57,14 +65,18 @@ public final class FlacBinarySearchSeekerTest extends InstrumentationTestCase {
public void testSetSeekTargetUs_returnsSeekPending()
throws IOException, FlacDecoderException, InterruptedException {
- byte[] data = TestUtil.getByteArray(getInstrumentation().getContext(), NOSEEKTABLE_FLAC);
+ byte[] data =
+ TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), NOSEEKTABLE_FLAC);
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
FlacDecoderJni decoderJni = new FlacDecoderJni();
decoderJni.setData(input);
FlacBinarySearchSeeker seeker =
new FlacBinarySearchSeeker(
- decoderJni.decodeMetadata(), /* firstFramePosition= */ 0, data.length, decoderJni);
+ decoderJni.decodeStreamMetadata(),
+ /* firstFramePosition= */ 0,
+ data.length,
+ decoderJni);
seeker.setSeekTargetUs(/* timeUs= */ 1000);
assertThat(seeker.isSeeking()).isTrue();
diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorSeekTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorSeekTest.java
index 58ab260277..3beb4d0103 100644
--- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorSeekTest.java
+++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorSeekTest.java
@@ -16,11 +16,13 @@
package com.google.android.exoplayer2.ext.flac;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
import android.content.Context;
import android.net.Uri;
-import android.support.annotation.Nullable;
-import android.test.InstrumentationTestCase;
+import androidx.annotation.Nullable;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.DefaultExtractorInput;
import com.google.android.exoplayer2.extractor.Extractor;
@@ -38,9 +40,12 @@ import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.util.List;
import java.util.Random;
+import org.junit.Before;
+import org.junit.runner.RunWith;
/** Seeking tests for {@link FlacExtractor} when the FLAC stream does not have a SEEKTABLE. */
-public final class FlacExtractorSeekTest extends InstrumentationTestCase {
+@RunWith(AndroidJUnit4.class)
+public final class FlacExtractorSeekTest {
private static final String NO_SEEKTABLE_FLAC = "bear_no_seek.flac";
private static final int DURATION_US = 2_741_000;
@@ -54,18 +59,18 @@ public final class FlacExtractorSeekTest extends InstrumentationTestCase {
private PositionHolder positionHolder;
private long totalInputLength;
- @Override
- protected void setUp() throws Exception {
- super.setUp();
+ @Before
+ public void setUp() throws Exception {
if (!FlacLibrary.isAvailable()) {
fail("Flac library not available.");
}
expectedOutput = new FakeExtractorOutput();
- extractAllSamplesFromFileToExpectedOutput(getInstrumentation().getContext(), NO_SEEKTABLE_FLAC);
+ extractAllSamplesFromFileToExpectedOutput(
+ ApplicationProvider.getApplicationContext(), NO_SEEKTABLE_FLAC);
expectedTrackOutput = expectedOutput.trackOutputs.get(0);
dataSource =
- new DefaultDataSourceFactory(getInstrumentation().getContext(), "UserAgent")
+ new DefaultDataSourceFactory(ApplicationProvider.getApplicationContext(), "UserAgent")
.createDataSource();
totalInputLength = readInputLength();
positionHolder = new PositionHolder();
@@ -223,7 +228,8 @@ public final class FlacExtractorSeekTest extends InstrumentationTestCase {
}
}
- private @Nullable SeekMap extractSeekMap(FlacExtractor extractor, FakeExtractorOutput output)
+ @Nullable
+ private SeekMap extractSeekMap(FlacExtractor extractor, FakeExtractorOutput output)
throws IOException, InterruptedException {
try {
ExtractorInput input = getExtractorInputFromPosition(0);
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 29a597daa4..97f152cea4 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
@@ -15,17 +15,20 @@
*/
package com.google.android.exoplayer2.ext.flac;
-import android.test.InstrumentationTestCase;
+import static org.junit.Assert.fail;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
+import org.junit.Before;
+import org.junit.runner.RunWith;
-/**
- * Unit test for {@link FlacExtractor}.
- */
-public class FlacExtractorTest extends InstrumentationTestCase {
+/** Unit test for {@link FlacExtractor}. */
+@RunWith(AndroidJUnit4.class)
+public class FlacExtractorTest {
- @Override
- protected void setUp() throws Exception {
- super.setUp();
+ @Before
+ public void setUp() {
if (!FlacLibrary.isAvailable()) {
fail("Flac library not available.");
}
@@ -33,11 +36,11 @@ public class FlacExtractorTest extends InstrumentationTestCase {
public void testExtractFlacSample() throws Exception {
ExtractorAsserts.assertBehavior(
- FlacExtractor::new, "bear.flac", getInstrumentation().getContext());
+ FlacExtractor::new, "bear.flac", ApplicationProvider.getApplicationContext());
}
public void testExtractFlacSampleWithId3Header() throws Exception {
ExtractorAsserts.assertBehavior(
- FlacExtractor::new, "bear_with_id3.flac", getInstrumentation().getContext());
+ FlacExtractor::new, "bear_with_id3.flac", ApplicationProvider.getApplicationContext());
}
}
diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java
index 99ddba55c4..c10d6fdb27 100644
--- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java
+++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java
@@ -15,21 +15,21 @@
*/
package com.google.android.exoplayer2.ext.flac;
-import static androidx.test.InstrumentationRegistry.getContext;
import static org.junit.Assert.fail;
import android.content.Context;
import android.net.Uri;
import android.os.Looper;
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
-import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import org.junit.Before;
@@ -56,7 +56,7 @@ public class FlacPlaybackTest {
private void playUri(String uri) throws Exception {
TestPlaybackRunnable testPlaybackRunnable =
- new TestPlaybackRunnable(Uri.parse(uri), getContext());
+ new TestPlaybackRunnable(Uri.parse(uri), ApplicationProvider.getApplicationContext());
Thread thread = new Thread(testPlaybackRunnable);
thread.start();
thread.join();
@@ -82,13 +82,13 @@ public class FlacPlaybackTest {
public void run() {
Looper.prepare();
LibflacAudioRenderer audioRenderer = new LibflacAudioRenderer();
- DefaultTrackSelector trackSelector = new DefaultTrackSelector();
- player = ExoPlayerFactory.newInstance(new Renderer[] {audioRenderer}, trackSelector);
+ DefaultTrackSelector trackSelector = new DefaultTrackSelector(context);
+ player = ExoPlayerFactory.newInstance(context, new Renderer[] {audioRenderer}, trackSelector);
player.addListener(this);
MediaSource mediaSource =
- new ExtractorMediaSource.Factory(
- new DefaultDataSourceFactory(context, "ExoPlayerExtFlacTest"))
- .setExtractorsFactory(MatroskaExtractor.FACTORY)
+ new ProgressiveMediaSource.Factory(
+ new DefaultDataSourceFactory(context, "ExoPlayerExtFlacTest"),
+ MatroskaExtractor.FACTORY)
.createMediaSource(uri);
player.prepare(mediaSource);
player.setPlayWhenReady(true);
@@ -101,7 +101,7 @@ public class FlacPlaybackTest {
}
@Override
- public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
+ public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
if (playbackState == Player.STATE_ENDED
|| (playbackState == Player.STATE_IDLE && playbackException != null)) {
player.release();
diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java
index b9c6ea06dd..4bfcc003ec 100644
--- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java
+++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java
@@ -19,7 +19,7 @@ import com.google.android.exoplayer2.extractor.BinarySearchSeeker;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.util.Assertions;
-import com.google.android.exoplayer2.util.FlacStreamInfo;
+import com.google.android.exoplayer2.util.FlacStreamMetadata;
import java.io.IOException;
import java.nio.ByteBuffer;
@@ -34,20 +34,20 @@ import java.nio.ByteBuffer;
private final FlacDecoderJni decoderJni;
public FlacBinarySearchSeeker(
- FlacStreamInfo streamInfo,
+ FlacStreamMetadata streamMetadata,
long firstFramePosition,
long inputLength,
FlacDecoderJni decoderJni) {
super(
- new FlacSeekTimestampConverter(streamInfo),
+ new FlacSeekTimestampConverter(streamMetadata),
new FlacTimestampSeeker(decoderJni),
- streamInfo.durationUs(),
+ streamMetadata.durationUs(),
/* floorTimePosition= */ 0,
- /* ceilingTimePosition= */ streamInfo.totalSamples,
+ /* ceilingTimePosition= */ streamMetadata.totalSamples,
/* floorBytePosition= */ firstFramePosition,
/* ceilingBytePosition= */ inputLength,
- /* approxBytesPerFrame= */ streamInfo.getApproxBytesPerFrame(),
- /* minimumSearchRange= */ Math.max(1, streamInfo.minFrameSize));
+ /* approxBytesPerFrame= */ streamMetadata.getApproxBytesPerFrame(),
+ /* minimumSearchRange= */ Math.max(1, streamMetadata.minFrameSize));
this.decoderJni = Assertions.checkNotNull(decoderJni);
}
@@ -112,15 +112,15 @@ import java.nio.ByteBuffer;
* the timestamp for a stream seek time position.
*/
private static final class FlacSeekTimestampConverter implements SeekTimestampConverter {
- private final FlacStreamInfo streamInfo;
+ private final FlacStreamMetadata streamMetadata;
- public FlacSeekTimestampConverter(FlacStreamInfo streamInfo) {
- this.streamInfo = streamInfo;
+ public FlacSeekTimestampConverter(FlacStreamMetadata streamMetadata) {
+ this.streamMetadata = streamMetadata;
}
@Override
public long timeUsToTargetTime(long timeUs) {
- return Assertions.checkNotNull(streamInfo).getSampleIndex(timeUs);
+ return Assertions.checkNotNull(streamMetadata).getSampleIndex(timeUs);
}
}
}
diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java
index 2d74bce5f1..50eb048d98 100644
--- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java
+++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java
@@ -15,11 +15,13 @@
*/
package com.google.android.exoplayer2.ext.flac;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.decoder.SimpleDecoder;
import com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
-import com.google.android.exoplayer2.util.FlacStreamInfo;
+import com.google.android.exoplayer2.util.FlacStreamMetadata;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.List;
@@ -56,21 +58,20 @@ import java.util.List;
}
decoderJni = new FlacDecoderJni();
decoderJni.setData(ByteBuffer.wrap(initializationData.get(0)));
- FlacStreamInfo streamInfo;
+ FlacStreamMetadata streamMetadata;
try {
- streamInfo = decoderJni.decodeMetadata();
+ streamMetadata = decoderJni.decodeStreamMetadata();
+ } catch (ParserException e) {
+ throw new FlacDecoderException("Failed to decode StreamInfo", e);
} catch (IOException | InterruptedException e) {
// Never happens.
throw new IllegalStateException(e);
}
- if (streamInfo == null) {
- throw new FlacDecoderException("Metadata decoding failed");
- }
int initialInputBufferSize =
- maxInputBufferSize != Format.NO_VALUE ? maxInputBufferSize : streamInfo.maxFrameSize;
+ maxInputBufferSize != Format.NO_VALUE ? maxInputBufferSize : streamMetadata.maxFrameSize;
setInitialInputBufferSize(initialInputBufferSize);
- maxOutputBufferSize = streamInfo.maxDecodedFrameSize();
+ maxOutputBufferSize = streamMetadata.maxDecodedFrameSize();
}
@Override
@@ -94,6 +95,7 @@ import java.util.List;
}
@Override
+ @Nullable
protected FlacDecoderException decode(
DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) {
if (reset) {
diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java
index de038921aa..f454e28c68 100644
--- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java
+++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java
@@ -15,9 +15,12 @@
*/
package com.google.android.exoplayer2.ext.flac;
+import androidx.annotation.Nullable;
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.FlacStreamInfo;
+import com.google.android.exoplayer2.util.FlacStreamMetadata;
+import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.nio.ByteBuffer;
@@ -37,14 +40,14 @@ import java.nio.ByteBuffer;
}
}
- private static final int TEMP_BUFFER_SIZE = 8192; // The same buffer size which libflac has
+ private static final int TEMP_BUFFER_SIZE = 8192; // The same buffer size as libflac.
private final long nativeDecoderContext;
- private ByteBuffer byteBufferData;
- private ExtractorInput extractorInput;
+ @Nullable private ByteBuffer byteBufferData;
+ @Nullable private ExtractorInput extractorInput;
+ @Nullable private byte[] tempBuffer;
private boolean endOfExtractorInput;
- private byte[] tempBuffer;
public FlacDecoderJni() throws FlacDecoderException {
if (!FlacLibrary.isAvailable()) {
@@ -57,67 +60,79 @@ import java.nio.ByteBuffer;
}
/**
- * Sets data to be parsed by libflac.
- * @param byteBufferData Source {@link ByteBuffer}
+ * Sets the data to be parsed.
+ *
+ * @param byteBufferData Source {@link ByteBuffer}.
*/
public void setData(ByteBuffer byteBufferData) {
this.byteBufferData = byteBufferData;
this.extractorInput = null;
- this.tempBuffer = null;
}
/**
- * Sets data to be parsed by libflac.
- * @param extractorInput Source {@link ExtractorInput}
+ * Sets the data to be parsed.
+ *
+ * @param extractorInput Source {@link ExtractorInput}.
*/
public void setData(ExtractorInput extractorInput) {
this.byteBufferData = null;
this.extractorInput = extractorInput;
- if (tempBuffer == null) {
- this.tempBuffer = new byte[TEMP_BUFFER_SIZE];
- }
endOfExtractorInput = false;
+ if (tempBuffer == null) {
+ tempBuffer = new byte[TEMP_BUFFER_SIZE];
+ }
}
+ /**
+ * Returns whether the end of the data to be parsed has been reached, or true if no data was set.
+ */
public boolean isEndOfData() {
if (byteBufferData != null) {
return byteBufferData.remaining() == 0;
} else if (extractorInput != null) {
return endOfExtractorInput;
+ } else {
+ return true;
}
- return true;
+ }
+
+ /** Clears the data to be parsed. */
+ public void clearData() {
+ byteBufferData = null;
+ extractorInput = null;
}
/**
* Reads up to {@code length} bytes from the data source.
- *
- * This method blocks until at least one byte of data can be read, the end of the input is
+ *
+ *
This method blocks until at least one byte of data can be read, the end of the input is
* detected or an exception is thrown.
- *
- * This method is called from the native code.
*
* @param target A target {@link ByteBuffer} into which data should be written.
- * @return Returns the number of bytes read, or -1 on failure. It's not an error if this returns
- * zero; it just means all the data read from the source.
+ * @return Returns the number of bytes read, or -1 on failure. If all of the data has already been
+ * read from the source, then 0 is returned.
*/
+ @SuppressWarnings("unused") // Called from native code.
public int read(ByteBuffer target) throws IOException, InterruptedException {
int byteCount = target.remaining();
if (byteBufferData != null) {
byteCount = Math.min(byteCount, byteBufferData.remaining());
int originalLimit = byteBufferData.limit();
byteBufferData.limit(byteBufferData.position() + byteCount);
-
target.put(byteBufferData);
-
byteBufferData.limit(originalLimit);
} else if (extractorInput != null) {
+ ExtractorInput extractorInput = this.extractorInput;
+ byte[] tempBuffer = Util.castNonNull(this.tempBuffer);
byteCount = Math.min(byteCount, TEMP_BUFFER_SIZE);
- int read = readFromExtractorInput(0, byteCount);
+ int read = readFromExtractorInput(extractorInput, tempBuffer, /* offset= */ 0, byteCount);
if (read < 4) {
// Reading less than 4 bytes, most of the time, happens because of getting the bytes left in
// the buffer of the input. Do another read to reduce the number of calls to this method
// from the native code.
- read += readFromExtractorInput(read, byteCount - read);
+ read +=
+ readFromExtractorInput(
+ extractorInput, tempBuffer, read, /* length= */ byteCount - read);
}
byteCount = read;
target.put(tempBuffer, 0, byteCount);
@@ -127,9 +142,13 @@ import java.nio.ByteBuffer;
return byteCount;
}
- /** Decodes and consumes the StreamInfo section from the FLAC stream. */
- public FlacStreamInfo decodeMetadata() throws IOException, InterruptedException {
- return flacDecodeMetadata(nativeDecoderContext);
+ /** Decodes and consumes the metadata from the FLAC stream. */
+ public FlacStreamMetadata decodeStreamMetadata() throws IOException, InterruptedException {
+ FlacStreamMetadata streamMetadata = flacDecodeMetadata(nativeDecoderContext);
+ if (streamMetadata == null) {
+ throw new ParserException("Failed to decode stream metadata");
+ }
+ return streamMetadata;
}
/**
@@ -234,7 +253,8 @@ import java.nio.ByteBuffer;
flacRelease(nativeDecoderContext);
}
- private int readFromExtractorInput(int offset, int length)
+ private int readFromExtractorInput(
+ ExtractorInput extractorInput, byte[] tempBuffer, int offset, int length)
throws IOException, InterruptedException {
int read = extractorInput.read(tempBuffer, offset, length);
if (read == C.RESULT_END_OF_INPUT) {
@@ -246,7 +266,7 @@ import java.nio.ByteBuffer;
private native long flacInit();
- private native FlacStreamInfo flacDecodeMetadata(long context)
+ private native FlacStreamMetadata flacDecodeMetadata(long context)
throws IOException, InterruptedException;
private native int flacDecodeToBuffer(long context, ByteBuffer outputBuffer)
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 a1fbcc69d6..cd91b06288 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,11 +17,11 @@ 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 androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
-import com.google.android.exoplayer2.extractor.BinarySearchSeeker;
+import com.google.android.exoplayer2.extractor.BinarySearchSeeker.OutputFrameHolder;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
@@ -33,14 +33,19 @@ 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.Assertions;
+import com.google.android.exoplayer2.util.FlacStreamMetadata;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray;
import java.io.IOException;
+import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.ByteBuffer;
import java.util.Arrays;
+import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/**
* Facilitates the extraction of data from the FLAC container format.
@@ -54,6 +59,7 @@ public final class FlacExtractor implements Extractor {
* Flags controlling the behavior of the extractor. Possible flag value is {@link
* #FLAG_DISABLE_ID3_METADATA}.
*/
+ @Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef(
flag = true,
@@ -72,27 +78,24 @@ public final class FlacExtractor implements Extractor {
*/
private static final byte[] FLAC_SIGNATURE = {'f', 'L', 'a', 'C', 0, 0, 0, 0x22};
+ private final ParsableByteArray outputBuffer;
private final Id3Peeker id3Peeker;
- private final boolean isId3MetadataDisabled;
+ private final boolean id3MetadataDisabled;
- private FlacDecoderJni decoderJni;
+ @Nullable private FlacDecoderJni decoderJni;
+ private @MonotonicNonNull ExtractorOutput extractorOutput;
+ private @MonotonicNonNull TrackOutput trackOutput;
- private ExtractorOutput extractorOutput;
- private TrackOutput trackOutput;
+ private boolean streamMetadataDecoded;
+ private @MonotonicNonNull FlacStreamMetadata streamMetadata;
+ private @MonotonicNonNull OutputFrameHolder outputFrameHolder;
- private ParsableByteArray outputBuffer;
- private ByteBuffer outputByteBuffer;
- private BinarySearchSeeker.OutputFrameHolder outputFrameHolder;
- private FlacStreamInfo streamInfo;
-
- private Metadata id3Metadata;
- private @Nullable FlacBinarySearchSeeker flacBinarySearchSeeker;
-
- private boolean readPastStreamInfo;
+ @Nullable private Metadata id3Metadata;
+ @Nullable private FlacBinarySearchSeeker binarySearchSeeker;
/** Constructs an instance with flags = 0. */
public FlacExtractor() {
- this(0);
+ this(/* flags= */ 0);
}
/**
@@ -101,8 +104,9 @@ public final class FlacExtractor implements Extractor {
* @param flags Flags that control the extractor's behavior.
*/
public FlacExtractor(int flags) {
+ outputBuffer = new ParsableByteArray();
id3Peeker = new Id3Peeker();
- isId3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0;
+ id3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0;
}
@Override
@@ -128,48 +132,53 @@ public final class FlacExtractor implements Extractor {
@Override
public int read(final ExtractorInput input, PositionHolder seekPosition)
throws IOException, InterruptedException {
- if (input.getPosition() == 0 && !isId3MetadataDisabled && id3Metadata == null) {
+ if (input.getPosition() == 0 && !id3MetadataDisabled && id3Metadata == null) {
id3Metadata = peekId3Data(input);
}
- decoderJni.setData(input);
- readPastStreamInfo(input);
-
- if (flacBinarySearchSeeker != null && flacBinarySearchSeeker.isSeeking()) {
- return handlePendingSeek(input, seekPosition);
- }
-
- long lastDecodePosition = decoderJni.getDecodePosition();
+ FlacDecoderJni decoderJni = initDecoderJni(input);
try {
- decoderJni.decodeSampleWithBacktrackPosition(outputByteBuffer, lastDecodePosition);
- } catch (FlacDecoderJni.FlacFrameDecodeException e) {
- throw new IOException("Cannot read frame at position " + lastDecodePosition, e);
- }
- int outputSize = outputByteBuffer.limit();
- if (outputSize == 0) {
- return RESULT_END_OF_INPUT;
- }
+ decodeStreamMetadata(input);
- writeLastSampleToOutput(outputSize, decoderJni.getLastFrameTimestamp());
- return decoderJni.isEndOfData() ? RESULT_END_OF_INPUT : RESULT_CONTINUE;
+ if (binarySearchSeeker != null && binarySearchSeeker.isSeeking()) {
+ return handlePendingSeek(input, seekPosition, outputBuffer, outputFrameHolder, trackOutput);
+ }
+
+ ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer;
+ long lastDecodePosition = decoderJni.getDecodePosition();
+ try {
+ decoderJni.decodeSampleWithBacktrackPosition(outputByteBuffer, lastDecodePosition);
+ } catch (FlacDecoderJni.FlacFrameDecodeException e) {
+ throw new IOException("Cannot read frame at position " + lastDecodePosition, e);
+ }
+ int outputSize = outputByteBuffer.limit();
+ if (outputSize == 0) {
+ return RESULT_END_OF_INPUT;
+ }
+
+ outputSample(outputBuffer, outputSize, decoderJni.getLastFrameTimestamp(), trackOutput);
+ return decoderJni.isEndOfData() ? RESULT_END_OF_INPUT : RESULT_CONTINUE;
+ } finally {
+ decoderJni.clearData();
+ }
}
@Override
public void seek(long position, long timeUs) {
if (position == 0) {
- readPastStreamInfo = false;
+ streamMetadataDecoded = false;
}
if (decoderJni != null) {
decoderJni.reset(position);
}
- if (flacBinarySearchSeeker != null) {
- flacBinarySearchSeeker.setSeekTargetUs(timeUs);
+ if (binarySearchSeeker != null) {
+ binarySearchSeeker.setSeekTargetUs(timeUs);
}
}
@Override
public void release() {
- flacBinarySearchSeeker = null;
+ binarySearchSeeker = null;
if (decoderJni != null) {
decoderJni.release();
decoderJni = null;
@@ -177,123 +186,141 @@ public final class FlacExtractor implements Extractor {
}
/**
- * Peeks ID3 tag data (if present) at the beginning of the input.
+ * Peeks ID3 tag data 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.
+ * @return The first ID3 tag {@link Metadata}, or null if an 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;
+ id3MetadataDisabled ? Id3Decoder.NO_FRAMES_PREDICATE : null;
return id3Peeker.peekId3Data(input, id3FramePredicate);
}
+ @EnsuresNonNull({"decoderJni", "extractorOutput", "trackOutput"}) // Ensures initialized.
+ @SuppressWarnings({"contracts.postcondition.not.satisfied"})
+ private FlacDecoderJni initDecoderJni(ExtractorInput input) {
+ FlacDecoderJni decoderJni = Assertions.checkNotNull(this.decoderJni);
+ decoderJni.setData(input);
+ return decoderJni;
+ }
+
+ @RequiresNonNull({"decoderJni", "extractorOutput", "trackOutput"}) // Requires initialized.
+ @EnsuresNonNull({"streamMetadata", "outputFrameHolder"}) // Ensures stream metadata decoded.
+ @SuppressWarnings({"contracts.postcondition.not.satisfied"})
+ private void decodeStreamMetadata(ExtractorInput input) throws InterruptedException, IOException {
+ if (streamMetadataDecoded) {
+ return;
+ }
+
+ FlacStreamMetadata streamMetadata;
+ try {
+ streamMetadata = decoderJni.decodeStreamMetadata();
+ } catch (IOException e) {
+ decoderJni.reset(/* newPosition= */ 0);
+ input.setRetryPosition(/* position= */ 0, e);
+ throw e;
+ }
+
+ streamMetadataDecoded = true;
+ if (this.streamMetadata == null) {
+ this.streamMetadata = streamMetadata;
+ binarySearchSeeker =
+ outputSeekMap(decoderJni, streamMetadata, input.getLength(), extractorOutput);
+ Metadata metadata = id3MetadataDisabled ? null : id3Metadata;
+ if (streamMetadata.metadata != null) {
+ metadata = streamMetadata.metadata.copyWithAppendedEntriesFrom(metadata);
+ }
+ outputFormat(streamMetadata, metadata, trackOutput);
+ outputBuffer.reset(streamMetadata.maxDecodedFrameSize());
+ outputFrameHolder = new OutputFrameHolder(ByteBuffer.wrap(outputBuffer.data));
+ }
+ }
+
+ @RequiresNonNull("binarySearchSeeker")
+ private int handlePendingSeek(
+ ExtractorInput input,
+ PositionHolder seekPosition,
+ ParsableByteArray outputBuffer,
+ OutputFrameHolder outputFrameHolder,
+ TrackOutput trackOutput)
+ throws InterruptedException, IOException {
+ int seekResult = binarySearchSeeker.handlePendingSeek(input, seekPosition, outputFrameHolder);
+ ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer;
+ if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) {
+ outputSample(outputBuffer, outputByteBuffer.limit(), outputFrameHolder.timeUs, trackOutput);
+ }
+ return seekResult;
+ }
+
/**
* 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 {
+ private static boolean peekFlacSignature(ExtractorInput input)
+ throws IOException, InterruptedException {
byte[] header = new byte[FLAC_SIGNATURE.length];
- input.peekFully(header, 0, FLAC_SIGNATURE.length);
+ input.peekFully(header, /* offset= */ 0, FLAC_SIGNATURE.length);
return Arrays.equals(header, FLAC_SIGNATURE);
}
- private void readPastStreamInfo(ExtractorInput input) throws InterruptedException, IOException {
- if (readPastStreamInfo) {
- return;
- }
-
- FlacStreamInfo streamInfo = decodeStreamInfo(input);
- readPastStreamInfo = true;
- if (this.streamInfo == null) {
- updateFlacStreamInfo(input, streamInfo);
- }
- }
-
- private void updateFlacStreamInfo(ExtractorInput input, FlacStreamInfo streamInfo) {
- this.streamInfo = streamInfo;
- outputSeekMap(input, streamInfo);
- outputFormat(streamInfo);
- outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize());
- outputByteBuffer = ByteBuffer.wrap(outputBuffer.data);
- outputFrameHolder = new BinarySearchSeeker.OutputFrameHolder(outputByteBuffer);
- }
-
- private FlacStreamInfo decodeStreamInfo(ExtractorInput input)
- throws InterruptedException, IOException {
- try {
- FlacStreamInfo streamInfo = decoderJni.decodeMetadata();
- if (streamInfo == null) {
- throw new IOException("Metadata decoding failed");
- }
- return streamInfo;
- } catch (IOException e) {
- decoderJni.reset(0);
- input.setRetryPosition(0, e);
- throw e;
- }
- }
-
- private void outputSeekMap(ExtractorInput input, FlacStreamInfo streamInfo) {
- boolean hasSeekTable = decoderJni.getSeekPosition(0) != -1;
- SeekMap seekMap =
- hasSeekTable
- ? new FlacSeekMap(streamInfo.durationUs(), decoderJni)
- : getSeekMapForNonSeekTableFlac(input, streamInfo);
- extractorOutput.seekMap(seekMap);
- }
-
- private SeekMap getSeekMapForNonSeekTableFlac(ExtractorInput input, FlacStreamInfo streamInfo) {
- long inputLength = input.getLength();
- if (inputLength != C.LENGTH_UNSET) {
+ /**
+ * Outputs a {@link SeekMap} and returns a {@link FlacBinarySearchSeeker} if one is required to
+ * handle seeks.
+ */
+ @Nullable
+ private static FlacBinarySearchSeeker outputSeekMap(
+ FlacDecoderJni decoderJni,
+ FlacStreamMetadata streamMetadata,
+ long streamLength,
+ ExtractorOutput output) {
+ boolean hasSeekTable = decoderJni.getSeekPosition(/* timeUs= */ 0) != -1;
+ FlacBinarySearchSeeker binarySearchSeeker = null;
+ SeekMap seekMap;
+ if (hasSeekTable) {
+ seekMap = new FlacSeekMap(streamMetadata.durationUs(), decoderJni);
+ } else if (streamLength != C.LENGTH_UNSET) {
long firstFramePosition = decoderJni.getDecodePosition();
- flacBinarySearchSeeker =
- new FlacBinarySearchSeeker(streamInfo, firstFramePosition, inputLength, decoderJni);
- return flacBinarySearchSeeker.getSeekMap();
- } else { // can't seek at all, because there's no SeekTable and the input length is unknown.
- return new SeekMap.Unseekable(streamInfo.durationUs());
+ binarySearchSeeker =
+ new FlacBinarySearchSeeker(streamMetadata, firstFramePosition, streamLength, decoderJni);
+ seekMap = binarySearchSeeker.getSeekMap();
+ } else {
+ seekMap = new SeekMap.Unseekable(streamMetadata.durationUs());
}
+ output.seekMap(seekMap);
+ return binarySearchSeeker;
}
- private void outputFormat(FlacStreamInfo streamInfo) {
+ private static void outputFormat(
+ FlacStreamMetadata streamMetadata, @Nullable Metadata metadata, TrackOutput output) {
Format mediaFormat =
Format.createAudioSampleFormat(
/* id= */ null,
MimeTypes.AUDIO_RAW,
/* codecs= */ null,
- streamInfo.bitRate(),
- streamInfo.maxDecodedFrameSize(),
- streamInfo.channels,
- streamInfo.sampleRate,
- getPcmEncoding(streamInfo.bitsPerSample),
+ streamMetadata.bitRate(),
+ streamMetadata.maxDecodedFrameSize(),
+ streamMetadata.channels,
+ streamMetadata.sampleRate,
+ getPcmEncoding(streamMetadata.bitsPerSample),
/* encoderDelay= */ 0,
/* encoderPadding= */ 0,
/* initializationData= */ null,
/* drmInitData= */ null,
/* selectionFlags= */ 0,
/* language= */ null,
- isId3MetadataDisabled ? null : id3Metadata);
- trackOutput.format(mediaFormat);
+ metadata);
+ output.format(mediaFormat);
}
- private int handlePendingSeek(ExtractorInput input, PositionHolder seekPosition)
- throws InterruptedException, IOException {
- int seekResult =
- flacBinarySearchSeeker.handlePendingSeek(input, seekPosition, outputFrameHolder);
- ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer;
- if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) {
- writeLastSampleToOutput(outputByteBuffer.limit(), outputFrameHolder.timeUs);
- }
- return seekResult;
- }
-
- private void writeLastSampleToOutput(int size, long lastSampleTimestamp) {
- outputBuffer.setPosition(0);
- trackOutput.sampleData(outputBuffer, size);
- trackOutput.sampleMetadata(lastSampleTimestamp, C.BUFFER_FLAG_KEY_FRAME, size, 0, null);
+ private static void outputSample(
+ ParsableByteArray sampleData, int size, long timeUs, TrackOutput output) {
+ sampleData.setPosition(0);
+ output.sampleData(sampleData, size);
+ output.sampleMetadata(
+ timeUs, C.BUFFER_FLAG_KEY_FRAME, size, /* offset= */ 0, /* encryptionData= */ null);
}
/** A {@link SeekMap} implementation using a SeekTable within the Flac stream. */
diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java
index fa66abbdc6..d833c47d14 100644
--- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java
+++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java
@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.ext.flac;
import android.os.Handler;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.audio.AudioProcessor;
@@ -33,7 +34,7 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
private static final int NUM_BUFFERS = 16;
public LibflacAudioRenderer() {
- this(null, null);
+ this(/* eventHandler= */ null, /* eventListener= */ null);
}
/**
@@ -42,18 +43,20 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
* @param eventListener A listener of events. May be null if delivery of events is not required.
* @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
*/
- public LibflacAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
+ public LibflacAudioRenderer(
+ @Nullable Handler eventHandler,
+ @Nullable AudioRendererEventListener eventListener,
AudioProcessor... audioProcessors) {
super(eventHandler, eventListener, audioProcessors);
}
@Override
- protected int supportsFormatInternal(DrmSessionManager drmSessionManager,
- Format format) {
+ protected int supportsFormatInternal(
+ @Nullable DrmSessionManager drmSessionManager, Format format) {
if (!FlacLibrary.isAvailable()
|| !MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType)) {
return FORMAT_UNSUPPORTED_TYPE;
- } else if (!supportsOutputEncoding(C.ENCODING_PCM_16BIT)) {
+ } else if (!supportsOutput(format.channelCount, C.ENCODING_PCM_16BIT)) {
return FORMAT_UNSUPPORTED_SUBTYPE;
} else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {
return FORMAT_UNSUPPORTED_DRM;
@@ -63,7 +66,7 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
}
@Override
- protected FlacDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto)
+ protected FlacDecoder createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto)
throws FlacDecoderException {
return new FlacDecoder(
NUM_BUFFERS, NUM_BUFFERS, format.maxInputSize, format.initializationData);
diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/package-info.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/package-info.java
new file mode 100644
index 0000000000..ef6da7e3c6
--- /dev/null
+++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+@NonNullApi
+package com.google.android.exoplayer2.ext.flac;
+
+import com.google.android.exoplayer2.util.NonNullApi;
diff --git a/extensions/flac/src/main/jni/Android.mk b/extensions/flac/src/main/jni/Android.mk
index ff54c1b3c0..69520a16e5 100644
--- a/extensions/flac/src/main/jni/Android.mk
+++ b/extensions/flac/src/main/jni/Android.mk
@@ -30,9 +30,9 @@ LOCAL_C_INCLUDES := \
$(LOCAL_PATH)/flac/src/libFLAC/include
LOCAL_SRC_FILES := $(FLAC_SOURCES)
-LOCAL_CFLAGS += '-DVERSION="1.3.1"' -DFLAC__NO_MD5 -DFLAC__INTEGER_ONLY_LIBRARY -DFLAC__NO_ASM
+LOCAL_CFLAGS += '-DPACKAGE_VERSION="1.3.2"' -DFLAC__NO_MD5 -DFLAC__INTEGER_ONLY_LIBRARY
LOCAL_CFLAGS += -D_REENTRANT -DPIC -DU_COMMON_IMPLEMENTATION -fPIC -DHAVE_SYS_PARAM_H
-LOCAL_CFLAGS += -O3 -funroll-loops -finline-functions
+LOCAL_CFLAGS += -O3 -funroll-loops -finline-functions -DFLAC__NO_ASM '-DFLAC__HAS_OGG=0'
LOCAL_LDLIBS := -llog -lz -lm
include $(BUILD_SHARED_LIBRARY)
diff --git a/extensions/flac/src/main/jni/Application.mk b/extensions/flac/src/main/jni/Application.mk
index 59bf5f8f87..eba20352f4 100644
--- a/extensions/flac/src/main/jni/Application.mk
+++ b/extensions/flac/src/main/jni/Application.mk
@@ -17,4 +17,4 @@
APP_OPTIM := release
APP_STL := gnustl_static
APP_CPPFLAGS := -frtti
-APP_PLATFORM := android-9
+APP_PLATFORM := android-14
diff --git a/extensions/flac/src/main/jni/flac_jni.cc b/extensions/flac/src/main/jni/flac_jni.cc
index 298719d48d..d60a7cead2 100644
--- a/extensions/flac/src/main/jni/flac_jni.cc
+++ b/extensions/flac/src/main/jni/flac_jni.cc
@@ -14,9 +14,12 @@
* limitations under the License.
*/
-#include
#include
+#include
+
#include
+#include
+
#include "include/flac_parser.h"
#define LOG_TAG "flac_jni"
@@ -95,19 +98,68 @@ DECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) {
return NULL;
}
+ jclass arrayListClass = env->FindClass("java/util/ArrayList");
+ jmethodID arrayListConstructor =
+ env->GetMethodID(arrayListClass, "", "()V");
+ jobject commentList = env->NewObject(arrayListClass, arrayListConstructor);
+ jmethodID arrayListAddMethod =
+ env->GetMethodID(arrayListClass, "add", "(Ljava/lang/Object;)Z");
+
+ if (context->parser->areVorbisCommentsValid()) {
+ std::vector vorbisComments =
+ context->parser->getVorbisComments();
+ for (std::vector::const_iterator vorbisComment =
+ vorbisComments.begin();
+ vorbisComment != vorbisComments.end(); ++vorbisComment) {
+ jstring commentString = env->NewStringUTF((*vorbisComment).c_str());
+ env->CallBooleanMethod(commentList, arrayListAddMethod, commentString);
+ env->DeleteLocalRef(commentString);
+ }
+ }
+
+ jobject pictureFrames = env->NewObject(arrayListClass, arrayListConstructor);
+ bool picturesValid = context->parser->arePicturesValid();
+ if (picturesValid) {
+ std::vector pictures = context->parser->getPictures();
+ jclass pictureFrameClass = env->FindClass(
+ "com/google/android/exoplayer2/metadata/flac/PictureFrame");
+ jmethodID pictureFrameConstructor =
+ env->GetMethodID(pictureFrameClass, "",
+ "(ILjava/lang/String;Ljava/lang/String;IIII[B)V");
+ for (std::vector::const_iterator picture = pictures.begin();
+ picture != pictures.end(); ++picture) {
+ jstring mimeType = env->NewStringUTF(picture->mimeType.c_str());
+ jstring description = env->NewStringUTF(picture->description.c_str());
+ jbyteArray pictureData = env->NewByteArray(picture->data.size());
+ env->SetByteArrayRegion(pictureData, 0, picture->data.size(),
+ (signed char *)&picture->data[0]);
+ jobject pictureFrame = env->NewObject(
+ pictureFrameClass, pictureFrameConstructor, picture->type, mimeType,
+ description, picture->width, picture->height, picture->depth,
+ picture->colors, pictureData);
+ env->CallBooleanMethod(pictureFrames, arrayListAddMethod, pictureFrame);
+ env->DeleteLocalRef(mimeType);
+ env->DeleteLocalRef(description);
+ env->DeleteLocalRef(pictureData);
+ }
+ }
+
const FLAC__StreamMetadata_StreamInfo &streamInfo =
context->parser->getStreamInfo();
- jclass cls = env->FindClass(
+ jclass flacStreamMetadataClass = env->FindClass(
"com/google/android/exoplayer2/util/"
- "FlacStreamInfo");
- jmethodID constructor = env->GetMethodID(cls, "", "(IIIIIIIJ)V");
+ "FlacStreamMetadata");
+ jmethodID flacStreamMetadataConstructor =
+ env->GetMethodID(flacStreamMetadataClass, "",
+ "(IIIIIIIJLjava/util/List;Ljava/util/List;)V");
- return env->NewObject(cls, constructor, streamInfo.min_blocksize,
- streamInfo.max_blocksize, streamInfo.min_framesize,
- streamInfo.max_framesize, streamInfo.sample_rate,
- streamInfo.channels, streamInfo.bits_per_sample,
- streamInfo.total_samples);
+ return env->NewObject(flacStreamMetadataClass, flacStreamMetadataConstructor,
+ streamInfo.min_blocksize, streamInfo.max_blocksize,
+ streamInfo.min_framesize, streamInfo.max_framesize,
+ streamInfo.sample_rate, streamInfo.channels,
+ streamInfo.bits_per_sample, streamInfo.total_samples,
+ commentList, pictureFrames);
}
DECODER_FUNC(jint, flacDecodeToBuffer, jlong jContext, jobject jOutputBuffer) {
diff --git a/extensions/flac/src/main/jni/flac_parser.cc b/extensions/flac/src/main/jni/flac_parser.cc
index 83d3367415..830f3e2178 100644
--- a/extensions/flac/src/main/jni/flac_parser.cc
+++ b/extensions/flac/src/main/jni/flac_parser.cc
@@ -172,6 +172,43 @@ void FLACParser::metadataCallback(const FLAC__StreamMetadata *metadata) {
case FLAC__METADATA_TYPE_SEEKTABLE:
mSeekTable = &metadata->data.seek_table;
break;
+ case FLAC__METADATA_TYPE_VORBIS_COMMENT:
+ if (!mVorbisCommentsValid) {
+ FLAC__StreamMetadata_VorbisComment vorbisComment =
+ metadata->data.vorbis_comment;
+ for (FLAC__uint32 i = 0; i < vorbisComment.num_comments; ++i) {
+ FLAC__StreamMetadata_VorbisComment_Entry vorbisCommentEntry =
+ vorbisComment.comments[i];
+ if (vorbisCommentEntry.entry != NULL) {
+ std::string comment(
+ reinterpret_cast(vorbisCommentEntry.entry),
+ vorbisCommentEntry.length);
+ mVorbisComments.push_back(comment);
+ }
+ }
+ mVorbisCommentsValid = true;
+ } else {
+ ALOGE("FLACParser::metadataCallback unexpected VORBISCOMMENT");
+ }
+ break;
+ case FLAC__METADATA_TYPE_PICTURE: {
+ const FLAC__StreamMetadata_Picture *parsedPicture =
+ &metadata->data.picture;
+ FlacPicture picture;
+ picture.mimeType.assign(std::string(parsedPicture->mime_type));
+ picture.description.assign(
+ std::string((char *)parsedPicture->description));
+ picture.data.assign(parsedPicture->data,
+ parsedPicture->data + parsedPicture->data_length);
+ picture.width = parsedPicture->width;
+ picture.height = parsedPicture->height;
+ picture.depth = parsedPicture->depth;
+ picture.colors = parsedPicture->colors;
+ picture.type = parsedPicture->type;
+ mPictures.push_back(picture);
+ mPicturesValid = true;
+ break;
+ }
default:
ALOGE("FLACParser::metadataCallback unexpected type %u", metadata->type);
break;
@@ -233,6 +270,8 @@ FLACParser::FLACParser(DataSource *source)
mCurrentPos(0LL),
mEOF(false),
mStreamInfoValid(false),
+ mVorbisCommentsValid(false),
+ mPicturesValid(false),
mWriteRequested(false),
mWriteCompleted(false),
mWriteBuffer(NULL),
@@ -266,6 +305,10 @@ bool FLACParser::init() {
FLAC__METADATA_TYPE_STREAMINFO);
FLAC__stream_decoder_set_metadata_respond(mDecoder,
FLAC__METADATA_TYPE_SEEKTABLE);
+ FLAC__stream_decoder_set_metadata_respond(mDecoder,
+ FLAC__METADATA_TYPE_VORBIS_COMMENT);
+ FLAC__stream_decoder_set_metadata_respond(mDecoder,
+ FLAC__METADATA_TYPE_PICTURE);
FLAC__StreamDecoderInitStatus initStatus;
initStatus = FLAC__stream_decoder_init_stream(
mDecoder, read_callback, seek_callback, tell_callback, length_callback,
diff --git a/extensions/flac/src/main/jni/include/flac_parser.h b/extensions/flac/src/main/jni/include/flac_parser.h
index cea7fbe33b..fd3e36a806 100644
--- a/extensions/flac/src/main/jni/include/flac_parser.h
+++ b/extensions/flac/src/main/jni/include/flac_parser.h
@@ -19,6 +19,10 @@
#include
+#include
+#include
+#include
+
// libFLAC parser
#include "FLAC/stream_decoder.h"
@@ -26,6 +30,17 @@
typedef int status_t;
+struct FlacPicture {
+ int type;
+ std::string mimeType;
+ std::string description;
+ FLAC__uint32 width;
+ FLAC__uint32 height;
+ FLAC__uint32 depth;
+ FLAC__uint32 colors;
+ std::vector data;
+};
+
class FLACParser {
public:
FLACParser(DataSource *source);
@@ -44,6 +59,16 @@ class FLACParser {
return mStreamInfo;
}
+ bool areVorbisCommentsValid() const { return mVorbisCommentsValid; }
+
+ const std::vector& getVorbisComments() const {
+ return mVorbisComments;
+ }
+
+ bool arePicturesValid() const { return mPicturesValid; }
+
+ const std::vector &getPictures() const { return mPictures; }
+
int64_t getLastFrameTimestamp() const {
return (1000000LL * mWriteHeader.number.sample_number) / getSampleRate();
}
@@ -71,6 +96,10 @@ class FLACParser {
mEOF = false;
if (newPosition == 0) {
mStreamInfoValid = false;
+ mVorbisCommentsValid = false;
+ mPicturesValid = false;
+ mVorbisComments.clear();
+ mPictures.clear();
FLAC__stream_decoder_reset(mDecoder);
} else {
FLAC__stream_decoder_flush(mDecoder);
@@ -116,6 +145,14 @@ class FLACParser {
const FLAC__StreamMetadata_SeekTable *mSeekTable;
uint64_t firstFrameOffset;
+ // cached when the VORBIS_COMMENT metadata is parsed by libFLAC
+ std::vector mVorbisComments;
+ bool mVorbisCommentsValid;
+
+ // cached when the PICTURE metadata is parsed by libFLAC
+ std::vector mPictures;
+ bool mPicturesValid;
+
// cached when a decoded PCM block is "written" by libFLAC parser
bool mWriteRequested;
bool mWriteCompleted;
diff --git a/extensions/flac/src/test/AndroidManifest.xml b/extensions/flac/src/test/AndroidManifest.xml
index 1d68b376ac..509151aa21 100644
--- a/extensions/flac/src/test/AndroidManifest.xml
+++ b/extensions/flac/src/test/AndroidManifest.xml
@@ -14,4 +14,6 @@
limitations under the License.
-->
-
+
+
+
diff --git a/extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultExtractorsFactoryTest.java b/extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultExtractorsFactoryTest.java
index 79c4452928..611197bbe5 100644
--- a/extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultExtractorsFactoryTest.java
+++ b/extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultExtractorsFactoryTest.java
@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.ext.flac;
import static com.google.common.truth.Truth.assertThat;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.amr.AmrExtractor;
@@ -27,6 +28,7 @@ import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor;
import com.google.android.exoplayer2.extractor.mp4.Mp4Extractor;
import com.google.android.exoplayer2.extractor.ogg.OggExtractor;
import com.google.android.exoplayer2.extractor.ts.Ac3Extractor;
+import com.google.android.exoplayer2.extractor.ts.Ac4Extractor;
import com.google.android.exoplayer2.extractor.ts.AdtsExtractor;
import com.google.android.exoplayer2.extractor.ts.PsExtractor;
import com.google.android.exoplayer2.extractor.ts.TsExtractor;
@@ -35,10 +37,9 @@ import java.util.ArrayList;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
/** Unit test for {@link DefaultExtractorsFactory}. */
-@RunWith(RobolectricTestRunner.class)
+@RunWith(AndroidJUnit4.class)
public final class DefaultExtractorsFactoryTest {
@Test
@@ -59,6 +60,7 @@ public final class DefaultExtractorsFactoryTest {
Mp3Extractor.class,
AdtsExtractor.class,
Ac3Extractor.class,
+ Ac4Extractor.class,
TsExtractor.class,
FlvExtractor.class,
OggExtractor.class,
diff --git a/extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultRenderersFactoryTest.java b/extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultRenderersFactoryTest.java
new file mode 100644
index 0000000000..fb20ff1114
--- /dev/null
+++ b/extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultRenderersFactoryTest.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2019 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.flac;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.testutil.DefaultRenderersFactoryAsserts;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit test for {@link DefaultRenderersFactoryTest} with {@link LibflacAudioRenderer}. */
+@RunWith(AndroidJUnit4.class)
+public final class DefaultRenderersFactoryTest {
+
+ @Test
+ public void createRenderers_instantiatesVpxRenderer() {
+ DefaultRenderersFactoryAsserts.assertExtensionRendererCreated(
+ LibflacAudioRenderer.class, C.TRACK_TYPE_AUDIO);
+ }
+}
diff --git a/extensions/flac/src/test/resources/robolectric.properties b/extensions/flac/src/test/resources/robolectric.properties
deleted file mode 100644
index 2f3210368e..0000000000
--- a/extensions/flac/src/test/resources/robolectric.properties
+++ /dev/null
@@ -1 +0,0 @@
-manifest=src/test/AndroidManifest.xml
diff --git a/extensions/gvr/README.md b/extensions/gvr/README.md
index 5dab885436..1874ff77d7 100644
--- a/extensions/gvr/README.md
+++ b/extensions/gvr/README.md
@@ -37,4 +37,4 @@ locally. Instructions for doing this can be found in ExoPlayer's
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.gvr.*`
belong to this module.
-[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
+[Javadoc]: https://exoplayer.dev/doc/reference/index.html
diff --git a/extensions/gvr/build.gradle b/extensions/gvr/build.gradle
index af973e1345..1031d6f4b7 100644
--- a/extensions/gvr/build.gradle
+++ b/extensions/gvr/build.gradle
@@ -16,7 +16,6 @@ apply plugin: 'com.android.library'
android {
compileSdkVersion project.ext.compileSdkVersion
- buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@@ -27,12 +26,16 @@ android {
minSdkVersion 19
targetSdkVersion project.ext.targetSdkVersion
}
+
+ testOptions.unitTests.includeAndroidResources = true
}
dependencies {
implementation project(modulePrefix + 'library-core')
- implementation 'com.android.support:support-annotations:' + supportLibraryVersion
- implementation 'com.google.vr:sdk-audio:1.80.0'
+ implementation project(modulePrefix + 'library-ui')
+ implementation 'androidx.annotation:annotation:1.1.0'
+ api 'com.google.vr:sdk-base:1.190.0'
+ compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
}
ext {
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 eca31c98e4..02e4328ec7 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,7 +15,7 @@
*/
package com.google.android.exoplayer2.ext.gvr;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.Format;
@@ -38,9 +38,11 @@ public final class GvrAudioProcessor implements AudioProcessor {
private static final int FRAMES_PER_OUTPUT_BUFFER = 1024;
private static final int OUTPUT_CHANNEL_COUNT = 2;
private static final int OUTPUT_FRAME_SIZE = OUTPUT_CHANNEL_COUNT * 2; // 16-bit stereo output.
+ private static final int NO_SURROUND_FORMAT = GvrAudioSurround.SurroundFormat.INVALID;
private int sampleRateHz;
private int channelCount;
+ private int pendingGvrAudioSurroundFormat;
@Nullable private GvrAudioSurround gvrAudioSurround;
private ByteBuffer buffer;
private boolean inputEnded;
@@ -57,6 +59,7 @@ public final class GvrAudioProcessor implements AudioProcessor {
sampleRateHz = Format.NO_VALUE;
channelCount = Format.NO_VALUE;
buffer = EMPTY_BUFFER;
+ pendingGvrAudioSurroundFormat = NO_SURROUND_FORMAT;
}
/**
@@ -92,33 +95,28 @@ public final class GvrAudioProcessor implements AudioProcessor {
}
this.sampleRateHz = sampleRateHz;
this.channelCount = channelCount;
- maybeReleaseGvrAudioSurround();
- int surroundFormat;
switch (channelCount) {
case 1:
- surroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_MONO;
+ pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_MONO;
break;
case 2:
- surroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_STEREO;
+ pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_STEREO;
break;
case 4:
- surroundFormat = GvrAudioSurround.SurroundFormat.FIRST_ORDER_AMBISONICS;
+ pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.FIRST_ORDER_AMBISONICS;
break;
case 6:
- surroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_FIVE_DOT_ONE;
+ pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_FIVE_DOT_ONE;
break;
case 9:
- surroundFormat = GvrAudioSurround.SurroundFormat.SECOND_ORDER_AMBISONICS;
+ pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.SECOND_ORDER_AMBISONICS;
break;
case 16:
- surroundFormat = GvrAudioSurround.SurroundFormat.THIRD_ORDER_AMBISONICS;
+ pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.THIRD_ORDER_AMBISONICS;
break;
default:
throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);
}
- gvrAudioSurround = new GvrAudioSurround(surroundFormat, sampleRateHz, channelCount,
- FRAMES_PER_OUTPUT_BUFFER);
- gvrAudioSurround.updateNativeOrientation(w, x, y, z);
if (buffer == EMPTY_BUFFER) {
buffer = ByteBuffer.allocateDirect(FRAMES_PER_OUTPUT_BUFFER * OUTPUT_FRAME_SIZE)
.order(ByteOrder.nativeOrder());
@@ -128,7 +126,7 @@ public final class GvrAudioProcessor implements AudioProcessor {
@Override
public boolean isActive() {
- return gvrAudioSurround != null;
+ return pendingGvrAudioSurroundFormat != NO_SURROUND_FORMAT || gvrAudioSurround != null;
}
@Override
@@ -156,14 +154,17 @@ public final class GvrAudioProcessor implements AudioProcessor {
@Override
public void queueEndOfStream() {
- Assertions.checkNotNull(gvrAudioSurround);
+ if (gvrAudioSurround != null) {
+ gvrAudioSurround.triggerProcessing();
+ }
inputEnded = true;
- gvrAudioSurround.triggerProcessing();
}
@Override
public ByteBuffer getOutput() {
- Assertions.checkNotNull(gvrAudioSurround);
+ if (gvrAudioSurround == null) {
+ return EMPTY_BUFFER;
+ }
int writtenBytes = gvrAudioSurround.getOutput(buffer, 0, buffer.capacity());
buffer.position(0).limit(writtenBytes);
return buffer;
@@ -171,13 +172,20 @@ public final class GvrAudioProcessor implements AudioProcessor {
@Override
public boolean isEnded() {
- Assertions.checkNotNull(gvrAudioSurround);
- return inputEnded && gvrAudioSurround.getAvailableOutputSize() == 0;
+ return inputEnded
+ && (gvrAudioSurround == null || gvrAudioSurround.getAvailableOutputSize() == 0);
}
@Override
public void flush() {
- if (gvrAudioSurround != null) {
+ if (pendingGvrAudioSurroundFormat != NO_SURROUND_FORMAT) {
+ maybeReleaseGvrAudioSurround();
+ gvrAudioSurround =
+ new GvrAudioSurround(
+ pendingGvrAudioSurroundFormat, sampleRateHz, channelCount, FRAMES_PER_OUTPUT_BUFFER);
+ gvrAudioSurround.updateNativeOrientation(w, x, y, z);
+ pendingGvrAudioSurroundFormat = NO_SURROUND_FORMAT;
+ } else if (gvrAudioSurround != null) {
gvrAudioSurround.flush();
}
inputEnded = false;
@@ -191,13 +199,13 @@ public final class GvrAudioProcessor implements AudioProcessor {
sampleRateHz = Format.NO_VALUE;
channelCount = Format.NO_VALUE;
buffer = EMPTY_BUFFER;
+ pendingGvrAudioSurroundFormat = NO_SURROUND_FORMAT;
}
private void maybeReleaseGvrAudioSurround() {
- if (this.gvrAudioSurround != null) {
- GvrAudioSurround gvrAudioSurround = this.gvrAudioSurround;
- this.gvrAudioSurround = null;
+ if (gvrAudioSurround != null) {
gvrAudioSurround.release();
+ gvrAudioSurround = null;
}
}
diff --git a/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrPlayerActivity.java b/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrPlayerActivity.java
new file mode 100644
index 0000000000..e22c97859a
--- /dev/null
+++ b/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrPlayerActivity.java
@@ -0,0 +1,358 @@
+/*
+ * 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.gvr;
+
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.SurfaceTexture;
+import android.opengl.Matrix;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import androidx.annotation.BinderThread;
+import androidx.annotation.CallSuper;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import android.view.ContextThemeWrapper;
+import android.view.MotionEvent;
+import android.view.Surface;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.ui.PlayerControlView;
+import com.google.android.exoplayer2.ui.spherical.GlViewGroup;
+import com.google.android.exoplayer2.ui.spherical.PointerRenderer;
+import com.google.android.exoplayer2.ui.spherical.SceneRenderer;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+import com.google.vr.ndk.base.DaydreamApi;
+import com.google.vr.sdk.base.AndroidCompat;
+import com.google.vr.sdk.base.Eye;
+import com.google.vr.sdk.base.GvrActivity;
+import com.google.vr.sdk.base.GvrView;
+import com.google.vr.sdk.base.HeadTransform;
+import com.google.vr.sdk.base.Viewport;
+import com.google.vr.sdk.controller.Controller;
+import com.google.vr.sdk.controller.ControllerManager;
+import javax.microedition.khronos.egl.EGLConfig;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+
+/**
+ * Base activity for VR 360 video playback. Before starting the video playback a player needs to be
+ * set using {@link #setPlayer(Player)}.
+ */
+public abstract class GvrPlayerActivity extends GvrActivity {
+
+ private static final int EXIT_FROM_VR_REQUEST_CODE = 42;
+
+ private final Handler mainHandler;
+
+ @Nullable private Player player;
+ private @MonotonicNonNull GlViewGroup glView;
+ private @MonotonicNonNull ControllerManager controllerManager;
+ private @MonotonicNonNull SurfaceTexture surfaceTexture;
+ private @MonotonicNonNull Surface surface;
+ private @MonotonicNonNull SceneRenderer scene;
+ private @MonotonicNonNull PlayerControlView playerControl;
+
+ public GvrPlayerActivity() {
+ mainHandler = new Handler(Looper.getMainLooper());
+ }
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setScreenAlwaysOn(true);
+
+ GvrView gvrView = new GvrView(this);
+ // Since videos typically have fewer pixels per degree than the phones, reducing the render
+ // target scaling factor reduces the work required to render the scene.
+ gvrView.setRenderTargetScale(.5f);
+
+ // If a custom theme isn't specified, the Context's theme is used. For VR Activities, this is
+ // the old Android default theme rather than a modern theme. Override this with a custom theme.
+ Context theme = new ContextThemeWrapper(this, R.style.VrTheme);
+ glView = new GlViewGroup(theme, R.layout.vr_ui);
+
+ playerControl = Assertions.checkNotNull(glView.findViewById(R.id.controller));
+ playerControl.setShowVrButton(true);
+ playerControl.setVrButtonListener(v -> exit());
+
+ PointerRenderer pointerRenderer = new PointerRenderer();
+ scene = new SceneRenderer();
+ Renderer renderer = new Renderer(scene, glView, pointerRenderer);
+
+ // Attach glView to gvrView in order to properly handle UI events.
+ gvrView.addView(glView, 0);
+
+ // Standard GvrView configuration
+ gvrView.setEGLConfigChooser(
+ 8, 8, 8, 8, // RGBA bits.
+ 16, // Depth bits.
+ 0); // Stencil bits.
+ gvrView.setRenderer(renderer);
+ setContentView(gvrView);
+
+ // Most Daydream phones can render a 4k video at 60fps in sustained performance mode. These
+ // options can be tweaked along with the render target scale.
+ if (gvrView.setAsyncReprojectionEnabled(true)) {
+ AndroidCompat.setSustainedPerformanceMode(this, true);
+ }
+
+ // Handle the user clicking on the 'X' in the top left corner. Since this is done when the user
+ // has taken the headset out of VR, it should launch the app's exit flow directly rather than
+ // using the transition flow.
+ gvrView.setOnCloseButtonListener(this::finish);
+
+ ControllerManager.EventListener listener =
+ new ControllerManager.EventListener() {
+ @Override
+ public void onApiStatusChanged(int status) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onRecentered() {
+ // TODO if in cardboard mode call gvrView.recenterHeadTracker();
+ glView.post(() -> Util.castNonNull(playerControl).show());
+ }
+ };
+ controllerManager = new ControllerManager(this, listener);
+
+ Controller controller = controllerManager.getController();
+ ControllerEventListener controllerEventListener =
+ new ControllerEventListener(controller, pointerRenderer, glView);
+ controller.setEventListener(controllerEventListener);
+ }
+
+ /**
+ * Sets the {@link Player} to use.
+ *
+ * @param newPlayer The {@link Player} to use, or {@code null} to detach the current player.
+ */
+ protected void setPlayer(@Nullable Player newPlayer) {
+ Assertions.checkNotNull(scene);
+ if (player == newPlayer) {
+ return;
+ }
+ if (player != null) {
+ Player.VideoComponent videoComponent = player.getVideoComponent();
+ if (videoComponent != null) {
+ if (surface != null) {
+ videoComponent.clearVideoSurface(surface);
+ }
+ videoComponent.clearVideoFrameMetadataListener(scene);
+ videoComponent.clearCameraMotionListener(scene);
+ }
+ }
+ player = newPlayer;
+ if (player != null) {
+ Player.VideoComponent videoComponent = player.getVideoComponent();
+ if (videoComponent != null) {
+ videoComponent.setVideoFrameMetadataListener(scene);
+ videoComponent.setCameraMotionListener(scene);
+ videoComponent.setVideoSurface(surface);
+ }
+ }
+ Assertions.checkNotNull(playerControl).setPlayer(player);
+ }
+
+ /**
+ * Sets the default stereo mode. If the played video doesn't contain a stereo mode the default one
+ * is used.
+ *
+ * @param stereoMode A {@link C.StereoMode} value.
+ */
+ protected void setDefaultStereoMode(@C.StereoMode int stereoMode) {
+ Assertions.checkNotNull(scene).setDefaultStereoMode(stereoMode);
+ }
+
+ @CallSuper
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent unused) {
+ if (requestCode == EXIT_FROM_VR_REQUEST_CODE && resultCode == RESULT_OK) {
+ finish();
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ Util.castNonNull(controllerManager).start();
+ }
+
+ @Override
+ protected void onPause() {
+ Util.castNonNull(controllerManager).stop();
+ super.onPause();
+ }
+
+ @Override
+ protected void onDestroy() {
+ setPlayer(null);
+ releaseSurface(surfaceTexture, surface);
+ super.onDestroy();
+ }
+
+ /** Tries to exit gracefully from VR using a VR transition dialog. */
+ @SuppressWarnings("nullness:argument.type.incompatible")
+ protected void exit() {
+ // This needs to use GVR's exit transition to avoid disorienting the user.
+ DaydreamApi api = DaydreamApi.create(this);
+ if (api != null) {
+ api.exitFromVr(this, EXIT_FROM_VR_REQUEST_CODE, null);
+ // Eventually, the Activity's onActivityResult will be called.
+ api.close();
+ } else {
+ finish();
+ }
+ }
+
+ /** Toggles PlayerControl visibility. */
+ @UiThread
+ protected void togglePlayerControlVisibility() {
+ if (Assertions.checkNotNull(playerControl).isVisible()) {
+ playerControl.hide();
+ } else {
+ playerControl.show();
+ }
+ }
+
+ // Called on GL thread.
+ private void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture) {
+ mainHandler.post(
+ () -> {
+ SurfaceTexture oldSurfaceTexture = this.surfaceTexture;
+ Surface oldSurface = this.surface;
+ this.surfaceTexture = surfaceTexture;
+ this.surface = new Surface(surfaceTexture);
+ if (player != null) {
+ Player.VideoComponent videoComponent = player.getVideoComponent();
+ if (videoComponent != null) {
+ videoComponent.setVideoSurface(surface);
+ }
+ }
+ releaseSurface(oldSurfaceTexture, oldSurface);
+ });
+ }
+
+ private static void releaseSurface(
+ @Nullable SurfaceTexture oldSurfaceTexture, @Nullable Surface oldSurface) {
+ if (oldSurfaceTexture != null) {
+ oldSurfaceTexture.release();
+ }
+ if (oldSurface != null) {
+ oldSurface.release();
+ }
+ }
+
+ private class Renderer implements GvrView.StereoRenderer {
+ private static final float Z_NEAR = .1f;
+ private static final float Z_FAR = 100;
+
+ private final float[] viewProjectionMatrix = new float[16];
+ private final SceneRenderer scene;
+ private final GlViewGroup glView;
+ private final PointerRenderer pointerRenderer;
+
+ public Renderer(SceneRenderer scene, GlViewGroup glView, PointerRenderer pointerRenderer) {
+ this.scene = scene;
+ this.glView = glView;
+ this.pointerRenderer = pointerRenderer;
+ }
+
+ @Override
+ public void onNewFrame(HeadTransform headTransform) {}
+
+ @Override
+ public void onDrawEye(Eye eye) {
+ Matrix.multiplyMM(
+ viewProjectionMatrix, 0, eye.getPerspective(Z_NEAR, Z_FAR), 0, eye.getEyeView(), 0);
+ scene.drawFrame(viewProjectionMatrix, eye.getType() == Eye.Type.RIGHT);
+ if (glView.isVisible()) {
+ glView.getRenderer().draw(viewProjectionMatrix);
+ pointerRenderer.draw(viewProjectionMatrix);
+ }
+ }
+
+ @Override
+ public void onFinishFrame(Viewport viewport) {}
+
+ @Override
+ public void onSurfaceCreated(EGLConfig config) {
+ onSurfaceTextureAvailable(scene.init());
+ glView.getRenderer().init();
+ pointerRenderer.init();
+ }
+
+ @Override
+ public void onSurfaceChanged(int width, int height) {}
+
+ @Override
+ public void onRendererShutdown() {
+ glView.getRenderer().shutdown();
+ pointerRenderer.shutdown();
+ scene.shutdown();
+ }
+ }
+
+ private class ControllerEventListener extends Controller.EventListener {
+
+ private final Controller controller;
+ private final PointerRenderer pointerRenderer;
+ private final GlViewGroup glView;
+ private final float[] controllerOrientationMatrix;
+ private boolean clickButtonDown;
+ private boolean appButtonDown;
+
+ public ControllerEventListener(
+ Controller controller, PointerRenderer pointerRenderer, GlViewGroup glView) {
+ this.controller = controller;
+ this.pointerRenderer = pointerRenderer;
+ this.glView = glView;
+ controllerOrientationMatrix = new float[16];
+ }
+
+ @Override
+ @BinderThread
+ public void onUpdate() {
+ controller.update();
+ controller.orientation.toRotationMatrix(controllerOrientationMatrix);
+ pointerRenderer.setControllerOrientation(controllerOrientationMatrix);
+
+ if (clickButtonDown || controller.clickButtonState) {
+ int action;
+ if (clickButtonDown != controller.clickButtonState) {
+ clickButtonDown = controller.clickButtonState;
+ action = clickButtonDown ? MotionEvent.ACTION_DOWN : MotionEvent.ACTION_UP;
+ } else {
+ action = MotionEvent.ACTION_MOVE;
+ }
+ glView.post(
+ () -> {
+ float[] angles = controller.orientation.toYawPitchRollRadians(new float[3]);
+ boolean clickedOnView = glView.simulateClick(action, angles[0], angles[1]);
+ if (action == MotionEvent.ACTION_DOWN && !clickedOnView) {
+ togglePlayerControlVisibility();
+ }
+ });
+ } else if (!appButtonDown && controller.appButtonState) {
+ glView.post(GvrPlayerActivity.this::togglePlayerControlVisibility);
+ }
+ appButtonDown = controller.appButtonState;
+ }
+ }
+}
diff --git a/extensions/gvr/src/main/res/layout/vr_ui.xml b/extensions/gvr/src/main/res/layout/vr_ui.xml
new file mode 100644
index 0000000000..e84ee31fe6
--- /dev/null
+++ b/extensions/gvr/src/main/res/layout/vr_ui.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
diff --git a/testutils_robolectric/src/main/AndroidManifest.xml b/extensions/gvr/src/main/res/values-v21/styles.xml
similarity index 88%
rename from testutils_robolectric/src/main/AndroidManifest.xml
rename to extensions/gvr/src/main/res/values-v21/styles.xml
index 057caad867..276db1b42d 100644
--- a/testutils_robolectric/src/main/AndroidManifest.xml
+++ b/extensions/gvr/src/main/res/values-v21/styles.xml
@@ -13,5 +13,6 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
-
+
+
+
diff --git a/extensions/gvr/src/main/res/values/styles.xml b/extensions/gvr/src/main/res/values/styles.xml
new file mode 100644
index 0000000000..ab5fde106a
--- /dev/null
+++ b/extensions/gvr/src/main/res/values/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
diff --git a/extensions/ima/README.md b/extensions/ima/README.md
index c5ef1af35f..4ed6a5428a 100644
--- a/extensions/ima/README.md
+++ b/extensions/ima/README.md
@@ -5,7 +5,7 @@ The IMA extension is an [AdsLoader][] implementation wrapping the
alongside content.
[IMA]: https://developers.google.com/interactive-media-ads/docs/sdks/android/
-[AdsLoader]: https://google.github.io/ExoPlayer/doc/reference/index.html?com/google/android/exoplayer2/source/ads/AdsLoader.html
+[AdsLoader]: https://exoplayer.dev/doc/reference/index.html?com/google/android/exoplayer2/source/ads/AdsLoader.html
## Getting the extension ##
@@ -30,7 +30,9 @@ To play ads alongside a single-window content `MediaSource`, prepare the player
with an `AdsMediaSource` constructed using an `ImaAdsLoader`, the content
`MediaSource` and an overlay `ViewGroup` on top of the player. Pass an ad tag
URI from your ad campaign when creating the `ImaAdsLoader`. The IMA
-documentation includes some [sample ad tags][] for testing.
+documentation includes some [sample ad tags][] for testing. Note that the IMA
+extension only supports players which are accessed on the application's main
+thread.
Resuming the player after entering the background requires some special handling
when playing ads. The player and its media source are released on entering the
@@ -59,4 +61,4 @@ playback.
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.ima.*`
belong to this module.
-[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
+[Javadoc]: https://exoplayer.dev/doc/reference/index.html
diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle
index 7fc7935cac..340e9832be 100644
--- a/extensions/ima/build.gradle
+++ b/extensions/ima/build.gradle
@@ -16,7 +16,6 @@ apply plugin: 'com.android.library'
android {
compileSdkVersion project.ext.compileSdkVersion
- buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@@ -28,24 +27,17 @@ android {
targetSdkVersion project.ext.targetSdkVersion
consumerProguardFiles 'proguard-rules.txt'
}
+
+ testOptions.unitTests.includeAndroidResources = true
}
dependencies {
- api 'com.google.ads.interactivemedia.v3:interactivemedia:3.9.4'
+ api 'com.google.ads.interactivemedia.v3:interactivemedia:3.11.3'
implementation project(modulePrefix + 'library-core')
- implementation 'com.google.android.gms:play-services-ads:15.0.1'
- // These dependencies are necessary to force the supportLibraryVersion of
- // com.android.support:support-v4 and com.android.support:customtabs to be
- // used. Else older versions are used, for example via:
- // com.google.android.gms:play-services-ads:15.0.1
- // |-- com.android.support:customtabs:26.1.0
- implementation 'com.android.support:support-v4:' + supportLibraryVersion
- implementation 'com.android.support:customtabs:' + supportLibraryVersion
- testImplementation 'com.google.truth:truth:' + truthVersion
- testImplementation 'junit:junit:' + junitVersion
- testImplementation 'org.mockito:mockito-core:' + mockitoVersion
+ implementation 'androidx.annotation:annotation:1.1.0'
+ implementation 'com.google.android.gms:play-services-ads-identifier:16.0.0'
+ testImplementation project(modulePrefix + 'testutils')
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
- testImplementation project(modulePrefix + 'testutils-robolectric')
}
ext {
diff --git a/extensions/ima/src/main/AndroidManifest.xml b/extensions/ima/src/main/AndroidManifest.xml
index 1bb79ff21d..226b15cb34 100644
--- a/extensions/ima/src/main/AndroidManifest.xml
+++ b/extensions/ima/src/main/AndroidManifest.xml
@@ -15,6 +15,10 @@
-->
-
+
+
+
+
diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java
index 56d62f26a9..e37f192c97 100644
--- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java
+++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java
@@ -17,10 +17,12 @@ package com.google.android.exoplayer2.ext.ima;
import android.content.Context;
import android.net.Uri;
+import android.os.Looper;
import android.os.SystemClock;
-import android.support.annotation.IntDef;
-import android.support.annotation.Nullable;
-import android.util.Log;
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import android.view.View;
import android.view.ViewGroup;
import com.google.ads.interactivemedia.v3.api.Ad;
import com.google.ads.interactivemedia.v3.api.AdDisplayContainer;
@@ -40,12 +42,12 @@ import com.google.ads.interactivemedia.v3.api.AdsRequest;
import com.google.ads.interactivemedia.v3.api.CompanionAdSlot;
import com.google.ads.interactivemedia.v3.api.ImaSdkFactory;
import com.google.ads.interactivemedia.v3.api.ImaSdkSettings;
+import com.google.ads.interactivemedia.v3.api.UiElement;
import com.google.ads.interactivemedia.v3.api.player.ContentProgressProvider;
import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer;
import com.google.ads.interactivemedia.v3.api.player.VideoProgressUpdate;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
-import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
@@ -56,19 +58,33 @@ import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
+import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
-/** Loads ads using the IMA SDK. All methods are called on the main thread. */
+/**
+ * {@link AdsLoader} using the IMA SDK. All methods must be called on the main thread.
+ *
+ * The player instance that will play the loaded ads must be set before playback using {@link
+ * #setPlayer(Player)}. If the ads loader is no longer required, it must be released by calling
+ * {@link #release()}.
+ *
+ *
The IMA SDK can take into account video control overlay views when calculating ad viewability.
+ * For more details see {@link AdDisplayContainer#registerVideoControlsOverlay(View)} and {@link
+ * AdViewProvider#getAdOverlayViews()}.
+ */
public final class ImaAdsLoader
implements Player.EventListener,
AdsLoader,
@@ -87,10 +103,13 @@ public final class ImaAdsLoader
private final Context context;
- private @Nullable ImaSdkSettings imaSdkSettings;
- private @Nullable AdEventListener adEventListener;
+ @Nullable private ImaSdkSettings imaSdkSettings;
+ @Nullable private AdEventListener adEventListener;
+ @Nullable private Set adUiElements;
private int vastLoadTimeoutMs;
private int mediaLoadTimeoutMs;
+ private int mediaBitrate;
+ private boolean focusSkipButtonWhenAvailable;
private ImaFactory imaFactory;
/**
@@ -102,6 +121,8 @@ public final class ImaAdsLoader
this.context = Assertions.checkNotNull(context);
vastLoadTimeoutMs = TIMEOUT_UNSET;
mediaLoadTimeoutMs = TIMEOUT_UNSET;
+ mediaBitrate = BITRATE_UNSET;
+ focusSkipButtonWhenAvailable = true;
imaFactory = new DefaultImaFactory();
}
@@ -131,6 +152,18 @@ public final class ImaAdsLoader
return this;
}
+ /**
+ * Sets the ad UI elements to be rendered by the IMA SDK.
+ *
+ * @param adUiElements The ad UI elements to be rendered by the IMA SDK.
+ * @return This builder, for convenience.
+ * @see AdsRenderingSettings#setUiElements(Set)
+ */
+ public Builder setAdUiElements(Set adUiElements) {
+ this.adUiElements = new HashSet<>(Assertions.checkNotNull(adUiElements));
+ return this;
+ }
+
/**
* Sets the VAST load timeout, in milliseconds.
*
@@ -139,7 +172,7 @@ public final class ImaAdsLoader
* @see AdsRequest#setVastLoadTimeout(float)
*/
public Builder setVastLoadTimeoutMs(int vastLoadTimeoutMs) {
- Assertions.checkArgument(vastLoadTimeoutMs >= 0);
+ Assertions.checkArgument(vastLoadTimeoutMs > 0);
this.vastLoadTimeoutMs = vastLoadTimeoutMs;
return this;
}
@@ -152,12 +185,39 @@ public final class ImaAdsLoader
* @see AdsRenderingSettings#setLoadVideoTimeout(int)
*/
public Builder setMediaLoadTimeoutMs(int mediaLoadTimeoutMs) {
- Assertions.checkArgument(mediaLoadTimeoutMs >= 0);
+ Assertions.checkArgument(mediaLoadTimeoutMs > 0);
this.mediaLoadTimeoutMs = mediaLoadTimeoutMs;
return this;
}
- // @VisibleForTesting
+ /**
+ * Sets the media maximum recommended bitrate for ads, in bps.
+ *
+ * @param bitrate The media maximum recommended bitrate for ads, in bps.
+ * @return This builder, for convenience.
+ * @see AdsRenderingSettings#setBitrateKbps(int)
+ */
+ public Builder setMaxMediaBitrate(int bitrate) {
+ Assertions.checkArgument(bitrate > 0);
+ this.mediaBitrate = bitrate;
+ return this;
+ }
+
+ /**
+ * Sets whether to focus the skip button (when available) on Android TV devices. The default
+ * setting is {@code true}.
+ *
+ * @param focusSkipButtonWhenAvailable Whether to focus the skip button (when available) on
+ * Android TV devices.
+ * @return This builder, for convenience.
+ * @see AdsRenderingSettings#setFocusSkipButtonWhenAvailable(boolean)
+ */
+ public Builder setFocusSkipButtonWhenAvailable(boolean focusSkipButtonWhenAvailable) {
+ this.focusSkipButtonWhenAvailable = focusSkipButtonWhenAvailable;
+ return this;
+ }
+
+ @VisibleForTesting
/* package */ Builder setImaFactory(ImaFactory imaFactory) {
this.imaFactory = Assertions.checkNotNull(imaFactory);
return this;
@@ -179,6 +239,9 @@ public final class ImaAdsLoader
null,
vastLoadTimeoutMs,
mediaLoadTimeoutMs,
+ mediaBitrate,
+ focusSkipButtonWhenAvailable,
+ adUiElements,
adEventListener,
imaFactory);
}
@@ -198,6 +261,9 @@ public final class ImaAdsLoader
adsResponse,
vastLoadTimeoutMs,
mediaLoadTimeoutMs,
+ mediaBitrate,
+ focusSkipButtonWhenAvailable,
+ adUiElements,
adEventListener,
imaFactory);
}
@@ -227,8 +293,10 @@ public final class ImaAdsLoader
private static final long MAXIMUM_PRELOAD_DURATION_MS = 8000;
private static final int TIMEOUT_UNSET = -1;
+ private static final int BITRATE_UNSET = -1;
/** The state of ad playback. */
+ @Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({IMA_AD_STATE_NONE, IMA_AD_STATE_PLAYING, IMA_AD_STATE_PAUSED})
private @interface ImaAdState {}
@@ -245,21 +313,25 @@ public final class ImaAdsLoader
*/
private static final int IMA_AD_STATE_PAUSED = 2;
- private final @Nullable Uri adTagUri;
- private final @Nullable String adsResponse;
+ @Nullable private final Uri adTagUri;
+ @Nullable private final String adsResponse;
private final int vastLoadTimeoutMs;
private final int mediaLoadTimeoutMs;
- private final @Nullable AdEventListener adEventListener;
+ private final boolean focusSkipButtonWhenAvailable;
+ private final int mediaBitrate;
+ @Nullable private final Set adUiElements;
+ @Nullable private final AdEventListener adEventListener;
private final ImaFactory imaFactory;
private final Timeline.Period period;
private final List adCallbacks;
private final AdDisplayContainer adDisplayContainer;
private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader;
+ @Nullable private Player nextPlayer;
private Object pendingAdRequestContext;
private List supportedMimeTypes;
- private EventListener eventListener;
- private Player player;
+ @Nullable private EventListener eventListener;
+ @Nullable private Player player;
private VideoProgressUpdate lastContentProgress;
private VideoProgressUpdate lastAdProgress;
private int lastVolumePercentage;
@@ -335,6 +407,9 @@ public final class ImaAdsLoader
/* adsResponse= */ null,
/* vastLoadTimeoutMs= */ TIMEOUT_UNSET,
/* mediaLoadTimeoutMs= */ TIMEOUT_UNSET,
+ /* mediaBitrate= */ BITRATE_UNSET,
+ /* focusSkipButtonWhenAvailable= */ true,
+ /* adUiElements= */ null,
/* adEventListener= */ null,
/* imaFactory= */ new DefaultImaFactory());
}
@@ -351,7 +426,7 @@ public final class ImaAdsLoader
* @deprecated Use {@link ImaAdsLoader.Builder}.
*/
@Deprecated
- public ImaAdsLoader(Context context, Uri adTagUri, ImaSdkSettings imaSdkSettings) {
+ public ImaAdsLoader(Context context, Uri adTagUri, @Nullable ImaSdkSettings imaSdkSettings) {
this(
context,
adTagUri,
@@ -359,6 +434,9 @@ public final class ImaAdsLoader
/* adsResponse= */ null,
/* vastLoadTimeoutMs= */ TIMEOUT_UNSET,
/* mediaLoadTimeoutMs= */ TIMEOUT_UNSET,
+ /* mediaBitrate= */ BITRATE_UNSET,
+ /* focusSkipButtonWhenAvailable= */ true,
+ /* adUiElements= */ null,
/* adEventListener= */ null,
/* imaFactory= */ new DefaultImaFactory());
}
@@ -370,6 +448,9 @@ public final class ImaAdsLoader
@Nullable String adsResponse,
int vastLoadTimeoutMs,
int mediaLoadTimeoutMs,
+ int mediaBitrate,
+ boolean focusSkipButtonWhenAvailable,
+ @Nullable Set adUiElements,
@Nullable AdEventListener adEventListener,
ImaFactory imaFactory) {
Assertions.checkArgument(adTagUri != null || adsResponse != null);
@@ -377,6 +458,9 @@ public final class ImaAdsLoader
this.adsResponse = adsResponse;
this.vastLoadTimeoutMs = vastLoadTimeoutMs;
this.mediaLoadTimeoutMs = mediaLoadTimeoutMs;
+ this.mediaBitrate = mediaBitrate;
+ this.focusSkipButtonWhenAvailable = focusSkipButtonWhenAvailable;
+ this.adUiElements = adUiElements;
this.adEventListener = adEventListener;
this.imaFactory = imaFactory;
if (imaSdkSettings == null) {
@@ -387,11 +471,11 @@ public final class ImaAdsLoader
}
imaSdkSettings.setPlayerType(IMA_SDK_SETTINGS_PLAYER_TYPE);
imaSdkSettings.setPlayerVersion(IMA_SDK_SETTINGS_PLAYER_VERSION);
- adsLoader = imaFactory.createAdsLoader(context, imaSdkSettings);
period = new Timeline.Period();
adCallbacks = new ArrayList<>(/* initialCapacity= */ 1);
adDisplayContainer = imaFactory.createAdDisplayContainer();
adDisplayContainer.setPlayer(/* videoAdPlayer= */ this);
+ adsLoader = imaFactory.createAdsLoader(context, imaSdkSettings, adDisplayContainer);
adsLoader.addAdErrorListener(/* adErrorListener= */ this);
adsLoader.addAdsLoadedListener(/* adsLoadedListener= */ this);
fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET;
@@ -409,13 +493,29 @@ public final class ImaAdsLoader
return adsLoader;
}
+ /**
+ * Returns the {@link AdDisplayContainer} used by this loader.
+ *
+ * Note: any video controls overlays registered via {@link
+ * AdDisplayContainer#registerVideoControlsOverlay(View)} will be unregistered automatically when
+ * the media source detaches from this instance. It is therefore necessary to re-register views
+ * each time the ads loader is reused. Alternatively, provide overlay views via the {@link
+ * AdsLoader.AdViewProvider} when creating the media source to benefit from automatic
+ * registration.
+ */
+ public AdDisplayContainer getAdDisplayContainer() {
+ return adDisplayContainer;
+ }
+
/**
* Sets the slots for displaying companion ads. Individual slots can be created using {@link
* ImaSdkFactory#createCompanionAdSlot()}.
*
* @param companionSlots Slots for displaying companion ads.
* @see AdDisplayContainer#setCompanionSlots(Collection)
+ * @deprecated Use {@code getAdDisplayContainer().setCompanionSlots(...)}.
*/
+ @Deprecated
public void setCompanionSlots(Collection companionSlots) {
adDisplayContainer.setCompanionSlots(companionSlots);
}
@@ -427,14 +527,14 @@ public final class ImaAdsLoader
* called, so it is only necessary to call this method if you want to request ads before preparing
* the player.
*
- * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI.
+ * @param adViewGroup A {@link ViewGroup} on top of the player that will show any ad UI.
*/
- public void requestAds(ViewGroup adUiViewGroup) {
+ public void requestAds(ViewGroup adViewGroup) {
if (adPlaybackState != null || adsManager != null || pendingAdRequestContext != null) {
// Ads have already been requested.
return;
}
- adDisplayContainer.setAdContainer(adUiViewGroup);
+ adDisplayContainer.setAdContainer(adViewGroup);
pendingAdRequestContext = new Object();
AdsRequest request = imaFactory.createAdsRequest();
if (adTagUri != null) {
@@ -445,7 +545,6 @@ public final class ImaAdsLoader
if (vastLoadTimeoutMs != TIMEOUT_UNSET) {
request.setVastLoadTimeout(vastLoadTimeoutMs);
}
- request.setAdDisplayContainer(adDisplayContainer);
request.setContentProgressProvider(this);
request.setUserRequestContext(pendingAdRequestContext);
adsLoader.requestAds(request);
@@ -453,6 +552,14 @@ public final class ImaAdsLoader
// AdsLoader implementation.
+ @Override
+ public void setPlayer(@Nullable Player player) {
+ Assertions.checkState(Looper.getMainLooper() == Looper.myLooper());
+ Assertions.checkState(
+ player == null || player.getApplicationLooper() == Looper.getMainLooper());
+ nextPlayer = player;
+ }
+
@Override
public void setSupportedContentTypes(@C.ContentType int... contentTypes) {
List supportedMimeTypes = new ArrayList<>();
@@ -477,13 +584,20 @@ public final class ImaAdsLoader
}
@Override
- public void attachPlayer(ExoPlayer player, EventListener eventListener, ViewGroup adUiViewGroup) {
- this.player = player;
+ public void start(EventListener eventListener, AdViewProvider adViewProvider) {
+ Assertions.checkNotNull(
+ nextPlayer, "Set player using adsLoader.setPlayer before preparing the player.");
+ player = nextPlayer;
this.eventListener = eventListener;
lastVolumePercentage = 0;
lastAdProgress = null;
lastContentProgress = null;
- adDisplayContainer.setAdContainer(adUiViewGroup);
+ ViewGroup adViewGroup = adViewProvider.getAdViewGroup();
+ adDisplayContainer.setAdContainer(adViewGroup);
+ View[] adOverlayViews = adViewProvider.getAdOverlayViews();
+ for (View view : adOverlayViews) {
+ adDisplayContainer.registerVideoControlsOverlay(view);
+ }
player.addListener(this);
maybeNotifyPendingAdLoadError();
if (adPlaybackState != null) {
@@ -497,12 +611,12 @@ public final class ImaAdsLoader
startAdPlayback();
} else {
// Ads haven't loaded yet, so request them.
- requestAds(adUiViewGroup);
+ requestAds(adViewGroup);
}
}
@Override
- public void detachPlayer() {
+ public void stop() {
if (adsManager != null && imaPausedContent) {
adPlaybackState =
adPlaybackState.withAdResumePositionUs(
@@ -512,6 +626,7 @@ public final class ImaAdsLoader
lastVolumePercentage = getVolume();
lastAdProgress = getAdProgress();
lastContentProgress = getContentProgress();
+ adDisplayContainer.unregisterAllVideoControlsOverlays();
player.removeListener(this);
player = null;
eventListener = null;
@@ -524,6 +639,8 @@ public final class ImaAdsLoader
adsManager.destroy();
adsManager = null;
}
+ adsLoader.removeAdsLoadedListener(/* adsLoadedListener= */ this);
+ adsLoader.removeAdErrorListener(/* adErrorListener= */ this);
imaPausedContent = false;
imaAdState = IMA_AD_STATE_NONE;
pendingAdLoadError = null;
@@ -639,7 +756,8 @@ public final class ImaAdsLoader
// until MAXIMUM_PRELOAD_DURATION_MS before the ad so that an ad group load error delivered
// just after an ad group isn't incorrectly attributed to the next ad group.
int nextAdGroupIndex =
- adPlaybackState.getAdGroupIndexAfterPositionUs(C.msToUs(contentPositionMs));
+ adPlaybackState.getAdGroupIndexAfterPositionUs(
+ C.msToUs(contentPositionMs), C.msToUs(contentDurationMs));
if (nextAdGroupIndex != expectedAdGroupIndex && nextAdGroupIndex != C.INDEX_UNSET) {
long nextAdGroupTimeMs = C.usToMs(adPlaybackState.adGroupTimesUs[nextAdGroupIndex]);
if (nextAdGroupTimeMs == C.TIME_END_OF_SOURCE) {
@@ -828,10 +946,9 @@ public final class ImaAdsLoader
// Player.EventListener implementation.
@Override
- public void onTimelineChanged(
- Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) {
- if (reason == Player.TIMELINE_CHANGE_REASON_RESET) {
- // The player is being reset and this source will be released.
+ public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) {
+ if (timeline.isEmpty()) {
+ // The player is being reset or contains no media.
return;
}
Assertions.checkArgument(timeline.getPeriodCount() == 1);
@@ -845,7 +962,7 @@ public final class ImaAdsLoader
}
@Override
- public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
+ public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
if (adsManager == null) {
return;
}
@@ -922,6 +1039,13 @@ public final class ImaAdsLoader
if (mediaLoadTimeoutMs != TIMEOUT_UNSET) {
adsRenderingSettings.setLoadVideoTimeout(mediaLoadTimeoutMs);
}
+ if (mediaBitrate != BITRATE_UNSET) {
+ adsRenderingSettings.setBitrateKbps(mediaBitrate / 1000);
+ }
+ adsRenderingSettings.setFocusSkipButtonWhenAvailable(focusSkipButtonWhenAvailable);
+ if (adUiElements != null) {
+ adsRenderingSettings.setUiElements(adUiElements);
+ }
// Set up the ad playback state, skipping ads based on the start position as required.
long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints());
@@ -929,13 +1053,8 @@ public final class ImaAdsLoader
long contentPositionMs = player.getCurrentPosition();
int adGroupIndexForPosition =
adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs));
- if (adGroupIndexForPosition == 0) {
- podIndexOffset = 0;
- } else if (adGroupIndexForPosition == C.INDEX_UNSET) {
- // There is no preroll and midroll pod indices start at 1.
- podIndexOffset = -1;
- } else /* adGroupIndexForPosition > 0 */ {
- // Skip ad groups before the one at or immediately before the playback position.
+ if (adGroupIndexForPosition > 0 && adGroupIndexForPosition != C.INDEX_UNSET) {
+ // Skip any ad groups before the one at or immediately before the playback position.
for (int i = 0; i < adGroupIndexForPosition; i++) {
adPlaybackState = adPlaybackState.withSkippedAdGroup(i);
}
@@ -945,9 +1064,18 @@ public final class ImaAdsLoader
long adGroupBeforeTimeUs = adGroupTimesUs[adGroupIndexForPosition - 1];
double midpointTimeUs = (adGroupForPositionTimeUs + adGroupBeforeTimeUs) / 2d;
adsRenderingSettings.setPlayAdsAfterTime(midpointTimeUs / C.MICROS_PER_SECOND);
+ }
- // We're removing one or more ads, which means that the earliest ad (if any) will be a
- // midroll/postroll. Midroll pod indices start at 1.
+ // IMA indexes any remaining midroll ad pods from 1. A preroll (if present) has index 0.
+ // Store an index offset as we want to index all ads (including skipped ones) from 0.
+ if (adGroupIndexForPosition == 0 && adGroupTimesUs[0] == 0) {
+ // We are playing a preroll.
+ podIndexOffset = 0;
+ } else if (adGroupIndexForPosition == C.INDEX_UNSET) {
+ // There's no ad to play which means there's no preroll.
+ podIndexOffset = -1;
+ } else {
+ // We are playing a midroll and any ads before it were skipped.
podIndexOffset = adGroupIndexForPosition - 1;
}
@@ -1249,7 +1377,8 @@ public final class ImaAdsLoader
private static boolean isAdGroupLoadError(AdError adError) {
// TODO: Find out what other errors need to be handled (if any), and whether each one relates to
// a single ad, ad group or the whole timeline.
- return adError.getErrorCode() == AdErrorCode.VAST_LINEAR_ASSET_MISMATCH;
+ return adError.getErrorCode() == AdErrorCode.VAST_LINEAR_ASSET_MISMATCH
+ || adError.getErrorCode() == AdErrorCode.UNKNOWN_ERROR;
}
private static boolean hasMidrollAdGroups(long[] adGroupTimesUs) {
@@ -1265,7 +1394,7 @@ public final class ImaAdsLoader
}
/** Factory for objects provided by the IMA SDK. */
- // @VisibleForTesting
+ @VisibleForTesting
/* package */ interface ImaFactory {
/** @see ImaSdkSettings */
ImaSdkSettings createImaSdkSettings();
@@ -1275,9 +1404,9 @@ public final class ImaAdsLoader
AdDisplayContainer createAdDisplayContainer();
/** @see com.google.ads.interactivemedia.v3.api.ImaSdkFactory#createAdsRequest() */
AdsRequest createAdsRequest();
- /** @see ImaSdkFactory#createAdsLoader(Context, ImaSdkSettings) */
+ /** @see ImaSdkFactory#createAdsLoader(Context, ImaSdkSettings, AdDisplayContainer) */
com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader(
- Context context, ImaSdkSettings imaSdkSettings);
+ Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer);
}
/** Default {@link ImaFactory} for non-test usage, which delegates to {@link ImaSdkFactory}. */
@@ -1304,8 +1433,9 @@ public final class ImaAdsLoader
@Override
public com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader(
- Context context, ImaSdkSettings imaSdkSettings) {
- return ImaSdkFactory.getInstance().createAdsLoader(context, imaSdkSettings);
+ Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer) {
+ return ImaSdkFactory.getInstance()
+ .createAdsLoader(context, imaSdkSettings, adDisplayContainer);
}
}
}
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
deleted file mode 100644
index 400061d019..0000000000
--- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java
+++ /dev/null
@@ -1,113 +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.ext.ima;
-
-import android.os.Handler;
-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.MediaSource.SourceInfoRefreshListener;
-import com.google.android.exoplayer2.source.ads.AdsMediaSource;
-import com.google.android.exoplayer2.upstream.Allocator;
-import com.google.android.exoplayer2.upstream.DataSource;
-import com.google.android.exoplayer2.upstream.TransferListener;
-import java.io.IOException;
-
-/**
- * A {@link MediaSource} that inserts ads linearly with a provided content media source.
- *
- * @deprecated Use com.google.android.exoplayer2.source.ads.AdsMediaSource with ImaAdsLoader.
- */
-@Deprecated
-public final class ImaAdsMediaSource extends BaseMediaSource implements SourceInfoRefreshListener {
-
- private final AdsMediaSource adsMediaSource;
-
- /**
- * Constructs a new source that inserts ads linearly with the content specified by
- * {@code contentMediaSource}.
- *
- * @param contentMediaSource The {@link MediaSource} providing the content to play.
- * @param dataSourceFactory Factory for data sources used to load ad media.
- * @param imaAdsLoader The loader for ads.
- * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI.
- */
- public ImaAdsMediaSource(MediaSource contentMediaSource, DataSource.Factory dataSourceFactory,
- ImaAdsLoader imaAdsLoader, ViewGroup adUiViewGroup) {
- this(contentMediaSource, dataSourceFactory, imaAdsLoader, adUiViewGroup, null, null);
- }
-
- /**
- * Constructs a new source that inserts ads linearly with the content specified by {@code
- * contentMediaSource}.
- *
- * @param contentMediaSource The {@link MediaSource} providing the content to play.
- * @param dataSourceFactory Factory for data sources used to load ad media.
- * @param imaAdsLoader The loader for ads.
- * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI.
- * @param eventHandler A handler for events. May be null if delivery of events is not required.
- * @param eventListener A listener of events. May be null if delivery of events is not required.
- */
- public ImaAdsMediaSource(
- MediaSource contentMediaSource,
- DataSource.Factory dataSourceFactory,
- ImaAdsLoader imaAdsLoader,
- ViewGroup adUiViewGroup,
- @Nullable Handler eventHandler,
- @Nullable AdsMediaSource.EventListener eventListener) {
- adsMediaSource = new AdsMediaSource(contentMediaSource, dataSourceFactory, imaAdsLoader,
- adUiViewGroup, eventHandler, eventListener);
- }
-
- @Override
- public void prepareSourceInternal(
- final ExoPlayer player,
- boolean isTopLevelSource,
- @Nullable TransferListener mediaTransferListener) {
- adsMediaSource.prepareSource(
- player, isTopLevelSource, /* listener= */ this, mediaTransferListener);
- }
-
- @Override
- public void maybeThrowSourceInfoRefreshError() throws IOException {
- adsMediaSource.maybeThrowSourceInfoRefreshError();
- }
-
- @Override
- public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
- return adsMediaSource.createPeriod(id, allocator);
- }
-
- @Override
- public void releasePeriod(MediaPeriod mediaPeriod) {
- adsMediaSource.releasePeriod(mediaPeriod);
- }
-
- @Override
- public void releaseSourceInternal() {
- adsMediaSource.releaseSource(/* listener= */ this);
- }
-
- @Override
- public void onSourceInfoRefreshed(
- MediaSource source, Timeline timeline, @Nullable Object manifest) {
- refreshSourceInfo(timeline, manifest);
- }
-}
diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/package-info.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/package-info.java
new file mode 100644
index 0000000000..9a382eb18f
--- /dev/null
+++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+@NonNullApi
+package com.google.android.exoplayer2.ext.ima;
+
+import com.google.android.exoplayer2.util.NonNullApi;
diff --git a/extensions/ima/src/main/proguard-rules.txt b/extensions/ima/src/main/proguard-rules.txt
deleted file mode 100644
index feef3daf7a..0000000000
--- a/extensions/ima/src/main/proguard-rules.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-# Proguard rules specific to the IMA extension.
-
--keep class com.google.ads.interactivemedia.** { *; }
--keep interface com.google.ads.interactivemedia.** { *; }
--keep class com.google.obf.** { *; }
--keep interface com.google.obf.** { *; }
diff --git a/extensions/ima/src/test/AndroidManifest.xml b/extensions/ima/src/test/AndroidManifest.xml
index 9a4e33189e..564c5d94dd 100644
--- a/extensions/ima/src/test/AndroidManifest.xml
+++ b/extensions/ima/src/test/AndroidManifest.xml
@@ -13,4 +13,6 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
+
+
+
diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAd.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAd.java
index 873a1b1d09..59dfc6473c 100644
--- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAd.java
+++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAd.java
@@ -64,6 +64,21 @@ import java.util.Set;
};
}
+ @Override
+ public int getVastMediaWidth() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int getVastMediaHeight() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int getVastMediaBitrate() {
+ throw new UnsupportedOperationException();
+ }
+
@Override
public boolean isSkippable() {
return skippable;
diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java
index c7026bab5f..a9572b7a8d 100644
--- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java
+++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java
@@ -15,22 +15,23 @@
*/
package com.google.android.exoplayer2.ext.ima;
+import android.os.Looper;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.testutil.StubExoPlayer;
+import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import java.util.ArrayList;
/** A fake player for testing content/ad playback. */
/* package */ final class FakePlayer extends StubExoPlayer {
private final ArrayList listeners;
- private final Timeline.Window window;
private final Timeline.Period period;
private final Timeline timeline;
private boolean prepared;
- private int state;
+ @Player.State private int state;
private boolean playWhenReady;
private long position;
private long contentPosition;
@@ -40,7 +41,6 @@ import java.util.ArrayList;
public FakePlayer() {
listeners = new ArrayList<>();
- window = new Timeline.Window();
period = new Timeline.Period();
state = Player.STATE_IDLE;
playWhenReady = true;
@@ -51,9 +51,7 @@ import java.util.ArrayList;
public void updateTimeline(Timeline timeline) {
for (Player.EventListener listener : listeners) {
listener.onTimelineChanged(
- timeline,
- null,
- prepared ? TIMELINE_CHANGE_REASON_DYNAMIC : TIMELINE_CHANGE_REASON_PREPARED);
+ timeline, prepared ? TIMELINE_CHANGE_REASON_DYNAMIC : TIMELINE_CHANGE_REASON_PREPARED);
}
prepared = true;
}
@@ -96,8 +94,8 @@ import java.util.ArrayList;
}
}
- /** Sets the state of this player with the given {@code STATE} constant. */
- public void setState(int state, boolean playWhenReady) {
+ /** Sets the {@link Player.State} of this player. */
+ public void setState(@Player.State int state, boolean playWhenReady) {
boolean notify = this.state != state || this.playWhenReady != playWhenReady;
this.state = state;
this.playWhenReady = playWhenReady;
@@ -110,6 +108,16 @@ import java.util.ArrayList;
// ExoPlayer methods. Other methods are unsupported.
+ @Override
+ public AudioComponent getAudioComponent() {
+ return null;
+ }
+
+ @Override
+ public Looper getApplicationLooper() {
+ return Looper.getMainLooper();
+ }
+
@Override
public void addListener(Player.EventListener listener) {
listeners.add(listener);
@@ -121,6 +129,7 @@ import java.util.ArrayList;
}
@Override
+ @Player.State
public int getPlaybackState() {
return state;
}
@@ -130,6 +139,16 @@ import java.util.ArrayList;
return playWhenReady;
}
+ @Override
+ public int getRendererCount() {
+ return 0;
+ }
+
+ @Override
+ public TrackSelectionArray getCurrentTrackSelections() {
+ return new TrackSelectionArray();
+ }
+
@Override
public Timeline getCurrentTimeline() {
return timeline;
@@ -145,16 +164,6 @@ import java.util.ArrayList;
return 0;
}
- @Override
- public int getNextWindowIndex() {
- return C.INDEX_UNSET;
- }
-
- @Override
- public int getPreviousWindowIndex() {
- return C.INDEX_UNSET;
- }
-
@Override
public long getDuration() {
if (timeline.isEmpty()) {
diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java
index b0fe731480..ab880703ee 100644
--- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java
+++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java
@@ -17,13 +17,17 @@ package com.google.android.exoplayer2.ext.ima;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.net.Uri;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
+import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.ads.interactivemedia.v3.api.Ad;
import com.google.ads.interactivemedia.v3.api.AdDisplayContainer;
import com.google.ads.interactivemedia.v3.api.AdEvent;
@@ -49,13 +53,12 @@ import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
/** Test for {@link ImaAdsLoader}. */
-@RunWith(RobolectricTestRunner.class)
+@RunWith(AndroidJUnit4.class)
public class ImaAdsLoaderTest {
private static final long CONTENT_DURATION_US = 10 * C.MICROS_PER_SECOND;
@@ -73,7 +76,9 @@ public class ImaAdsLoaderTest {
private @Mock AdDisplayContainer adDisplayContainer;
private @Mock AdsManager adsManager;
private SingletonImaFactory testImaFactory;
- private ViewGroup adUiViewGroup;
+ private ViewGroup adViewGroup;
+ private View adOverlayView;
+ private AdsLoader.AdViewProvider adViewProvider;
private TestAdsLoaderListener adsLoaderListener;
private FakePlayer fakeExoPlayer;
private ImaAdsLoader imaAdsLoader;
@@ -90,7 +95,20 @@ public class ImaAdsLoaderTest {
adDisplayContainer,
fakeAdsRequest,
fakeAdsLoader);
- adUiViewGroup = new FrameLayout(RuntimeEnvironment.application);
+ adViewGroup = new FrameLayout(ApplicationProvider.getApplicationContext());
+ adOverlayView = new View(ApplicationProvider.getApplicationContext());
+ adViewProvider =
+ new AdsLoader.AdViewProvider() {
+ @Override
+ public ViewGroup getAdViewGroup() {
+ return adViewGroup;
+ }
+
+ @Override
+ public View[] getAdOverlayViews() {
+ return new View[] {adOverlayView};
+ }
+ };
}
@After
@@ -109,17 +127,18 @@ public class ImaAdsLoaderTest {
}
@Test
- public void testAttachPlayer_setsAdUiViewGroup() {
+ public void testStart_setsAdUiViewGroup() {
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
- imaAdsLoader.attachPlayer(fakeExoPlayer, adsLoaderListener, adUiViewGroup);
+ imaAdsLoader.start(adsLoaderListener, adViewProvider);
- verify(adDisplayContainer, atLeastOnce()).setAdContainer(adUiViewGroup);
+ verify(adDisplayContainer, atLeastOnce()).setAdContainer(adViewGroup);
+ verify(adDisplayContainer, atLeastOnce()).registerVideoControlsOverlay(adOverlayView);
}
@Test
- public void testAttachPlayer_updatesAdPlaybackState() {
+ public void testStart_updatesAdPlaybackState() {
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
- imaAdsLoader.attachPlayer(fakeExoPlayer, adsLoaderListener, adUiViewGroup);
+ imaAdsLoader.start(adsLoaderListener, adViewProvider);
assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo(
@@ -128,17 +147,17 @@ public class ImaAdsLoaderTest {
}
@Test
- public void testAttachAfterRelease() {
+ public void testStartAfterRelease() {
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
imaAdsLoader.release();
- imaAdsLoader.attachPlayer(fakeExoPlayer, adsLoaderListener, adUiViewGroup);
+ imaAdsLoader.start(adsLoaderListener, adViewProvider);
}
@Test
- public void testAttachAndCallbacksAfterRelease() {
+ public void testStartAndCallbacksAfterRelease() {
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
imaAdsLoader.release();
- imaAdsLoader.attachPlayer(fakeExoPlayer, adsLoaderListener, adUiViewGroup);
+ imaAdsLoader.start(adsLoaderListener, adViewProvider);
fakeExoPlayer.setPlayingContentPosition(/* position= */ 0);
fakeExoPlayer.setState(Player.STATE_READY, true);
@@ -146,7 +165,7 @@ public class ImaAdsLoaderTest {
// Note: we can't currently call getContentProgress/getAdProgress as a VerifyError is thrown
// when using Robolectric and accessing VideoProgressUpdate.VIDEO_TIME_NOT_READY, due to the IMA
// SDK being proguarded.
- imaAdsLoader.requestAds(adUiViewGroup);
+ imaAdsLoader.requestAds(adViewGroup);
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.LOADED, UNSKIPPABLE_AD));
imaAdsLoader.loadAd(TEST_URI.toString());
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, UNSKIPPABLE_AD));
@@ -166,7 +185,7 @@ public class ImaAdsLoaderTest {
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
// Load the preroll ad.
- imaAdsLoader.attachPlayer(fakeExoPlayer, adsLoaderListener, adUiViewGroup);
+ imaAdsLoader.start(adsLoaderListener, adViewProvider);
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.LOADED, UNSKIPPABLE_AD));
imaAdsLoader.loadAd(TEST_URI.toString());
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, UNSKIPPABLE_AD));
@@ -201,15 +220,28 @@ public class ImaAdsLoaderTest {
.withAdResumePositionUs(/* adResumePositionUs= */ 0));
}
+ @Test
+ public void testStop_unregistersAllVideoControlOverlays() {
+ setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
+ imaAdsLoader.start(adsLoaderListener, adViewProvider);
+ imaAdsLoader.requestAds(adViewGroup);
+ imaAdsLoader.stop();
+
+ InOrder inOrder = inOrder(adDisplayContainer);
+ inOrder.verify(adDisplayContainer).registerVideoControlsOverlay(adOverlayView);
+ inOrder.verify(adDisplayContainer).unregisterAllVideoControlsOverlays();
+ }
+
private void setupPlayback(Timeline contentTimeline, long[][] adDurationsUs, Float[] cuePoints) {
fakeExoPlayer = new FakePlayer();
adsLoaderListener = new TestAdsLoaderListener(fakeExoPlayer, contentTimeline, adDurationsUs);
when(adsManager.getAdCuePoints()).thenReturn(Arrays.asList(cuePoints));
imaAdsLoader =
- new ImaAdsLoader.Builder(RuntimeEnvironment.application)
+ new ImaAdsLoader.Builder(ApplicationProvider.getApplicationContext())
.setImaFactory(testImaFactory)
.setImaSdkSettings(imaSdkSettings)
.buildForAdTag(TEST_URI);
+ imaAdsLoader.setPlayer(fakeExoPlayer);
}
private static AdEvent getAdEvent(AdEventType adEventType, @Nullable Ad ad) {
@@ -220,7 +252,8 @@ public class ImaAdsLoaderTest {
}
@Override
- public @Nullable Ad getAd() {
+ @Nullable
+ public Ad getAd() {
return ad;
}
diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/SingletonImaFactory.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/SingletonImaFactory.java
index dd46d8a68b..4efd8cf38c 100644
--- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/SingletonImaFactory.java
+++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/SingletonImaFactory.java
@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.ext.ima;
import android.content.Context;
import com.google.ads.interactivemedia.v3.api.AdDisplayContainer;
+import com.google.ads.interactivemedia.v3.api.AdsLoader;
import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings;
import com.google.ads.interactivemedia.v3.api.AdsRequest;
import com.google.ads.interactivemedia.v3.api.ImaSdkSettings;
@@ -64,8 +65,8 @@ final class SingletonImaFactory implements ImaAdsLoader.ImaFactory {
}
@Override
- public com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader(
- Context context, ImaSdkSettings imaSdkSettings) {
+ public AdsLoader createAdsLoader(
+ Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer) {
return adsLoader;
}
}
diff --git a/extensions/ima/src/test/resources/robolectric.properties b/extensions/ima/src/test/resources/robolectric.properties
deleted file mode 100644
index 2f3210368e..0000000000
--- a/extensions/ima/src/test/resources/robolectric.properties
+++ /dev/null
@@ -1 +0,0 @@
-manifest=src/test/AndroidManifest.xml
diff --git a/extensions/jobdispatcher/README.md b/extensions/jobdispatcher/README.md
index f70125ba38..a6f0c3966a 100644
--- a/extensions/jobdispatcher/README.md
+++ b/extensions/jobdispatcher/README.md
@@ -1,7 +1,11 @@
# ExoPlayer Firebase JobDispatcher extension #
+**DEPRECATED - Please use [WorkManager extension][] or [PlatformScheduler][] instead.**
+
This extension provides a Scheduler implementation which uses [Firebase JobDispatcher][].
+[WorkManager extension]: https://github.com/google/ExoPlayer/blob/release-v2/extensions/workmanager/README.md
+[PlatformScheduler]: https://github.com/google/ExoPlayer/blob/release-v2/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java
[Firebase JobDispatcher]: https://github.com/firebase/firebase-jobdispatcher-android
## Getting the extension ##
@@ -20,4 +24,3 @@ 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
index a0e3f8e0c8..d7f19d2545 100644
--- a/extensions/jobdispatcher/build.gradle
+++ b/extensions/jobdispatcher/build.gradle
@@ -18,7 +18,6 @@ apply plugin: 'com.android.library'
android {
compileSdkVersion project.ext.compileSdkVersion
- buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@@ -29,6 +28,8 @@ android {
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
}
+
+ testOptions.unitTests.includeAndroidResources = true
}
dependencies {
diff --git a/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java b/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java
index d6759245c0..c8975275f1 100644
--- a/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java
+++ b/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java
@@ -18,7 +18,6 @@ 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;
@@ -29,6 +28,7 @@ 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.Assertions;
+import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
/**
@@ -54,9 +54,13 @@ import com.google.android.exoplayer2.util.Util;
*
* @see GoogleApiAvailability
+ * @deprecated Use com.google.android.exoplayer2.ext.workmanager.WorkManagerScheduler or {@link
+ * com.google.android.exoplayer2.scheduler.PlatformScheduler}.
*/
+@Deprecated
public final class JobDispatcherScheduler implements Scheduler {
+ private static final boolean DEBUG = false;
private static final String TAG = "JobDispatcherScheduler";
private static final String KEY_SERVICE_ACTION = "service_action";
private static final String KEY_SERVICE_PACKAGE = "service_package";
@@ -78,8 +82,8 @@ public final class JobDispatcherScheduler implements Scheduler {
}
@Override
- public boolean schedule(Requirements requirements, String serviceAction, String servicePackage) {
- Job job = buildJob(jobDispatcher, requirements, jobTag, serviceAction, servicePackage);
+ public boolean schedule(Requirements requirements, String servicePackage, String serviceAction) {
+ Job job = buildJob(jobDispatcher, requirements, jobTag, servicePackage, serviceAction);
int result = jobDispatcher.schedule(job);
logd("Scheduling job: " + jobTag + " result: " + result);
return result == FirebaseJobDispatcher.SCHEDULE_RESULT_SUCCESS;
@@ -96,26 +100,18 @@ public final class JobDispatcherScheduler implements Scheduler {
FirebaseJobDispatcher dispatcher,
Requirements requirements,
String tag,
- String serviceAction,
- String servicePackage) {
+ String servicePackage,
+ String serviceAction) {
Job.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.isUnmeteredNetworkRequired()) {
+ builder.addConstraint(Constraint.ON_UNMETERED_NETWORK);
+ } else if (requirements.isNetworkRequired()) {
+ builder.addConstraint(Constraint.ON_ANY_NETWORK);
}
if (requirements.isIdleRequired()) {
@@ -129,7 +125,7 @@ public final class JobDispatcherScheduler implements Scheduler {
Bundle extras = new Bundle();
extras.putString(KEY_SERVICE_ACTION, serviceAction);
extras.putString(KEY_SERVICE_PACKAGE, servicePackage);
- extras.putInt(KEY_REQUIREMENTS, requirements.getRequirementsData());
+ extras.putInt(KEY_REQUIREMENTS, requirements.getRequirements());
builder.setExtras(extras);
return builder.build();
diff --git a/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/package-info.java b/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/package-info.java
new file mode 100644
index 0000000000..a66904b505
--- /dev/null
+++ b/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+@NonNullApi
+package com.google.android.exoplayer2.ext.jobdispatcher;
+
+import com.google.android.exoplayer2.util.NonNullApi;
diff --git a/extensions/leanback/README.md b/extensions/leanback/README.md
index 4eba6552e1..b6eb085247 100644
--- a/extensions/leanback/README.md
+++ b/extensions/leanback/README.md
@@ -28,4 +28,4 @@ locally. Instructions for doing this can be found in ExoPlayer's
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.leanback.*`
belong to this module.
-[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
+[Javadoc]: https://exoplayer.dev/doc/reference/index.html
diff --git a/extensions/leanback/build.gradle b/extensions/leanback/build.gradle
index 10bfef8e7c..ecaa78e25b 100644
--- a/extensions/leanback/build.gradle
+++ b/extensions/leanback/build.gradle
@@ -16,7 +16,6 @@ apply plugin: 'com.android.library'
android {
compileSdkVersion project.ext.compileSdkVersion
- buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@@ -27,11 +26,14 @@ android {
minSdkVersion 17
targetSdkVersion project.ext.targetSdkVersion
}
+
+ testOptions.unitTests.includeAndroidResources = true
}
dependencies {
implementation project(modulePrefix + 'library-core')
- implementation('com.android.support:leanback-v17:' + supportLibraryVersion)
+ implementation 'androidx.annotation:annotation:1.1.0'
+ implementation 'androidx.leanback:leanback:1.0.0'
}
ext {
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 0c9491bb1a..370e5515e8 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
@@ -17,14 +17,14 @@ package com.google.android.exoplayer2.ext.leanback;
import android.content.Context;
import android.os.Handler;
-import android.support.annotation.Nullable;
-import android.support.v17.leanback.R;
-import android.support.v17.leanback.media.PlaybackGlueHost;
-import android.support.v17.leanback.media.PlayerAdapter;
-import android.support.v17.leanback.media.SurfaceHolderGlueHost;
+import androidx.annotation.Nullable;
import android.util.Pair;
import android.view.Surface;
import android.view.SurfaceHolder;
+import androidx.leanback.R;
+import androidx.leanback.media.PlaybackGlueHost;
+import androidx.leanback.media.PlayerAdapter;
+import androidx.leanback.media.SurfaceHolderGlueHost;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ControlDispatcher;
import com.google.android.exoplayer2.DefaultControlDispatcher;
@@ -51,10 +51,10 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab
private final ComponentListener componentListener;
private final int updatePeriodMs;
- private @Nullable PlaybackPreparer playbackPreparer;
+ @Nullable private PlaybackPreparer playbackPreparer;
private ControlDispatcher controlDispatcher;
- private @Nullable ErrorMessageProvider super ExoPlaybackException> errorMessageProvider;
- private @Nullable SurfaceHolderGlueHost surfaceHolderGlueHost;
+ @Nullable private ErrorMessageProvider super ExoPlaybackException> errorMessageProvider;
+ @Nullable private SurfaceHolderGlueHost surfaceHolderGlueHost;
private boolean hasSurface;
private boolean lastNotifiedPreparedState;
@@ -271,7 +271,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab
// Player.EventListener implementation.
@Override
- public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
+ public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
notifyStateChanged();
}
@@ -288,8 +288,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab
}
@Override
- public void onTimelineChanged(
- Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {
+ public void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) {
Callback callback = getCallback();
callback.onDurationChanged(LeanbackPlayerAdapter.this);
callback.onCurrentPositionChanged(LeanbackPlayerAdapter.this);
diff --git a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/package-info.java b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/package-info.java
new file mode 100644
index 0000000000..79c544fc0f
--- /dev/null
+++ b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+@NonNullApi
+package com.google.android.exoplayer2.ext.leanback;
+
+import com.google.android.exoplayer2.util.NonNullApi;
diff --git a/extensions/mediasession/README.md b/extensions/mediasession/README.md
index bd6b59c0c1..64b55a8036 100644
--- a/extensions/mediasession/README.md
+++ b/extensions/mediasession/README.md
@@ -29,4 +29,4 @@ locally. Instructions for doing this can be found in ExoPlayer's
* [Javadoc][]: Classes matching
`com.google.android.exoplayer2.ext.mediasession.*` belong to this module.
-[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
+[Javadoc]: https://exoplayer.dev/doc/reference/index.html
diff --git a/extensions/mediasession/build.gradle b/extensions/mediasession/build.gradle
index da04b0aec3..7ee973723c 100644
--- a/extensions/mediasession/build.gradle
+++ b/extensions/mediasession/build.gradle
@@ -16,7 +16,6 @@ apply plugin: 'com.android.library'
android {
compileSdkVersion project.ext.compileSdkVersion
- buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@@ -27,11 +26,14 @@ android {
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
}
+
+ testOptions.unitTests.includeAndroidResources = true
}
dependencies {
implementation project(modulePrefix + 'library-core')
- implementation 'com.android.support:support-media-compat:' + supportLibraryVersion
+ api 'androidx.media:media:1.0.1'
+ compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
}
ext {
diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java
deleted file mode 100644
index 7d983e14e9..0000000000
--- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java
+++ /dev/null
@@ -1,173 +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.ext.mediasession;
-
-import android.os.Bundle;
-import android.os.ResultReceiver;
-import android.support.v4.media.session.PlaybackStateCompat;
-import com.google.android.exoplayer2.C;
-import com.google.android.exoplayer2.Player;
-import com.google.android.exoplayer2.util.RepeatModeUtil;
-
-/**
- * A default implementation of {@link MediaSessionConnector.PlaybackController}.
- *
- * Methods can be safely overridden by subclasses to intercept calls for given actions.
- */
-public class DefaultPlaybackController implements MediaSessionConnector.PlaybackController {
-
- /**
- * The default fast forward increment, in milliseconds.
- */
- public static final int DEFAULT_FAST_FORWARD_MS = 15000;
- /**
- * The default rewind increment, in milliseconds.
- */
- public static final int DEFAULT_REWIND_MS = 5000;
-
- private static final long BASE_ACTIONS = PlaybackStateCompat.ACTION_PLAY_PAUSE
- | PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE
- | PlaybackStateCompat.ACTION_STOP | PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE
- | PlaybackStateCompat.ACTION_SET_REPEAT_MODE;
-
- protected final long rewindIncrementMs;
- protected final long fastForwardIncrementMs;
- protected final int repeatToggleModes;
-
- /**
- * Creates a new instance.
- *
- * Equivalent to {@code DefaultPlaybackController(DefaultPlaybackController.DEFAULT_REWIND_MS,
- * DefaultPlaybackController.DEFAULT_FAST_FORWARD_MS,
- * MediaSessionConnector.DEFAULT_REPEAT_TOGGLE_MODES)}.
- */
- public DefaultPlaybackController() {
- this(DEFAULT_REWIND_MS, DEFAULT_FAST_FORWARD_MS,
- MediaSessionConnector.DEFAULT_REPEAT_TOGGLE_MODES);
- }
-
- /**
- * Creates a new instance with the given fast forward and rewind increments.
- * @param rewindIncrementMs The rewind increment in milliseconds. A zero or negative value will
- * cause the rewind action to be disabled.
- * @param fastForwardIncrementMs The fast forward increment in milliseconds. A zero or negative
- * @param repeatToggleModes The available repeatToggleModes.
- */
- public DefaultPlaybackController(long rewindIncrementMs, long fastForwardIncrementMs,
- @RepeatModeUtil.RepeatToggleModes int repeatToggleModes) {
- this.rewindIncrementMs = rewindIncrementMs;
- this.fastForwardIncrementMs = fastForwardIncrementMs;
- this.repeatToggleModes = repeatToggleModes;
- }
-
- @Override
- public long getSupportedPlaybackActions(Player player) {
- if (player == null || player.getCurrentTimeline().isEmpty()) {
- return 0;
- } else if (!player.isCurrentWindowSeekable()) {
- return BASE_ACTIONS;
- }
- long actions = BASE_ACTIONS | PlaybackStateCompat.ACTION_SEEK_TO;
- if (fastForwardIncrementMs > 0) {
- actions |= PlaybackStateCompat.ACTION_FAST_FORWARD;
- }
- if (rewindIncrementMs > 0) {
- actions |= PlaybackStateCompat.ACTION_REWIND;
- }
- return actions;
- }
-
- @Override
- public void onPlay(Player player) {
- player.setPlayWhenReady(true);
- }
-
- @Override
- public void onPause(Player player) {
- player.setPlayWhenReady(false);
- }
-
- @Override
- public void onSeekTo(Player player, long position) {
- long duration = player.getDuration();
- if (duration != C.TIME_UNSET) {
- position = Math.min(position, duration);
- }
- player.seekTo(Math.max(position, 0));
- }
-
- @Override
- public void onFastForward(Player player) {
- if (fastForwardIncrementMs <= 0) {
- return;
- }
- onSeekTo(player, player.getCurrentPosition() + fastForwardIncrementMs);
- }
-
- @Override
- public void onRewind(Player player) {
- if (rewindIncrementMs <= 0) {
- return;
- }
- onSeekTo(player, player.getCurrentPosition() - rewindIncrementMs);
- }
-
- @Override
- public void onStop(Player player) {
- player.stop(true);
- }
-
- @Override
- public void onSetShuffleMode(Player player, int shuffleMode) {
- player.setShuffleModeEnabled(shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL
- || shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_GROUP);
- }
-
- @Override
- public void onSetRepeatMode(Player player, int repeatMode) {
- int selectedExoPlayerRepeatMode = player.getRepeatMode();
- switch (repeatMode) {
- case PlaybackStateCompat.REPEAT_MODE_ALL:
- case PlaybackStateCompat.REPEAT_MODE_GROUP:
- if ((repeatToggleModes & RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL) != 0) {
- selectedExoPlayerRepeatMode = Player.REPEAT_MODE_ALL;
- }
- break;
- case PlaybackStateCompat.REPEAT_MODE_ONE:
- if ((repeatToggleModes & RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE) != 0) {
- selectedExoPlayerRepeatMode = Player.REPEAT_MODE_ONE;
- }
- break;
- default:
- selectedExoPlayerRepeatMode = Player.REPEAT_MODE_OFF;
- break;
- }
- player.setRepeatMode(selectedExoPlayerRepeatMode);
- }
-
- // CommandReceiver implementation.
-
- @Override
- public String[] getCommands() {
- return null;
- }
-
- @Override
- public void onCommand(Player player, String command, Bundle extras, ResultReceiver cb) {
- // Do nothing.
- }
-
-}
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 f1d3e8fbd0..cb1788f2fc 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
@@ -15,14 +15,17 @@
*/
package com.google.android.exoplayer2.ext.mediasession;
+import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
+import android.os.Looper;
import android.os.ResultReceiver;
import android.os.SystemClock;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
+import androidx.annotation.LongDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.RatingCompat;
@@ -31,22 +34,34 @@ import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.util.Pair;
import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ControlDispatcher;
+import com.google.android.exoplayer2.DefaultControlDispatcher;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.ErrorMessageProvider;
import com.google.android.exoplayer2.util.RepeatModeUtil;
import com.google.android.exoplayer2.util.Util;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
/**
* Connects a {@link MediaSessionCompat} to a {@link Player}.
*
+ *
This connector does not call {@link MediaSessionCompat#setActive(boolean)}, and so
+ * application code is responsible for making the session active when desired. A session must be
+ * active for transport controls to be displayed (e.g. on the lock screen) and for it to receive
+ * media button events.
+ *
*
The connector listens for actions sent by the media session's controller and implements these
* actions by calling appropriate player methods. The playback state of the media session is
* automatically synced with the player. The connector can also be optionally extended by providing
@@ -54,18 +69,24 @@ import java.util.Map;
*
*
* Actions to initiate media playback ({@code PlaybackStateCompat#ACTION_PREPARE_*} and {@code
- * PlaybackStateCompat#ACTION_PLAY_*}) can be handled by a {@link PlaybackPreparer} passed
- * when calling {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}. Custom
- * actions can be handled by passing one or more {@link CustomActionProvider}s in a similar
- * way.
+ * PlaybackStateCompat#ACTION_PLAY_*}) can be handled by a {@link PlaybackPreparer} passed to
+ * {@link #setPlaybackPreparer(PlaybackPreparer)}.
+ * Custom actions can be handled by passing one or more {@link CustomActionProvider}s to
+ * {@link #setCustomActionProviders(CustomActionProvider...)}.
* To enable a media queue and navigation within it, you can set a {@link QueueNavigator} by
* calling {@link #setQueueNavigator(QueueNavigator)}. Use of {@link TimelineQueueNavigator}
* is recommended for most use cases.
* To enable editing of the media queue, you can set a {@link QueueEditor} by calling {@link
* #setQueueEditor(QueueEditor)}.
+ * A {@link MediaButtonEventHandler} can be set by calling {@link
+ * #setMediaButtonEventHandler(MediaButtonEventHandler)}. By default media button events are
+ * handled by {@link MediaSessionCompat.Callback#onMediaButtonEvent(Intent)}.
* An {@link ErrorMessageProvider} for providing human readable error messages and
* corresponding error codes can be set by calling {@link
* #setErrorMessageProvider(ErrorMessageProvider)}.
+ * A {@link MediaMetadataProvider} can be set by calling {@link
+ * #setMediaMetadataProvider(MediaMetadataProvider)}. By default the {@link
+ * DefaultMediaMetadataProvider} is used.
*
*/
public final class MediaSessionConnector {
@@ -74,31 +95,85 @@ public final class MediaSessionConnector {
ExoPlayerLibraryInfo.registerModule("goog.exo.mediasession");
}
- /**
- * The default repeat toggle modes which is the bitmask of {@link
- * RepeatModeUtil#REPEAT_TOGGLE_MODE_ONE} and {@link RepeatModeUtil#REPEAT_TOGGLE_MODE_ALL}.
- */
- public static final @RepeatModeUtil.RepeatToggleModes int DEFAULT_REPEAT_TOGGLE_MODES =
- RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE | RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL;
+ /** Playback actions supported by the connector. */
+ @LongDef(
+ flag = true,
+ value = {
+ PlaybackStateCompat.ACTION_PLAY_PAUSE,
+ PlaybackStateCompat.ACTION_PLAY,
+ PlaybackStateCompat.ACTION_PAUSE,
+ PlaybackStateCompat.ACTION_SEEK_TO,
+ PlaybackStateCompat.ACTION_FAST_FORWARD,
+ PlaybackStateCompat.ACTION_REWIND,
+ PlaybackStateCompat.ACTION_STOP,
+ PlaybackStateCompat.ACTION_SET_REPEAT_MODE,
+ PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface PlaybackActions {}
+
+ @PlaybackActions
+ public static final long ALL_PLAYBACK_ACTIONS =
+ PlaybackStateCompat.ACTION_PLAY_PAUSE
+ | PlaybackStateCompat.ACTION_PLAY
+ | PlaybackStateCompat.ACTION_PAUSE
+ | PlaybackStateCompat.ACTION_SEEK_TO
+ | PlaybackStateCompat.ACTION_FAST_FORWARD
+ | PlaybackStateCompat.ACTION_REWIND
+ | PlaybackStateCompat.ACTION_STOP
+ | PlaybackStateCompat.ACTION_SET_REPEAT_MODE
+ | PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE;
+
+ /** The default playback actions. */
+ @PlaybackActions public static final long DEFAULT_PLAYBACK_ACTIONS = ALL_PLAYBACK_ACTIONS;
+
+ /** The default fast forward increment, in milliseconds. */
+ public static final int DEFAULT_FAST_FORWARD_MS = 15000;
+ /** The default rewind increment, in milliseconds. */
+ public static final int DEFAULT_REWIND_MS = 5000;
public static final String EXTRAS_PITCH = "EXO_PITCH";
+
+ private static final long BASE_PLAYBACK_ACTIONS =
+ PlaybackStateCompat.ACTION_PLAY_PAUSE
+ | PlaybackStateCompat.ACTION_PLAY
+ | PlaybackStateCompat.ACTION_PAUSE
+ | PlaybackStateCompat.ACTION_STOP
+ | PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE
+ | PlaybackStateCompat.ACTION_SET_REPEAT_MODE;
private static final int BASE_MEDIA_SESSION_FLAGS =
MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS
| MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS;
private static final int EDITOR_MEDIA_SESSION_FLAGS =
BASE_MEDIA_SESSION_FLAGS | MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS;
+ private static final MediaMetadataCompat METADATA_EMPTY =
+ new MediaMetadataCompat.Builder().build();
+
/** Receiver of media commands sent by a media controller. */
public interface CommandReceiver {
/**
- * Returns the commands the receiver handles, or {@code null} if no commands need to be handled.
+ * See {@link MediaSessionCompat.Callback#onCommand(String, Bundle, ResultReceiver)}. The
+ * receiver may handle the command, but is not required to do so. Changes to the player should
+ * be made via the {@link ControlDispatcher}.
+ *
+ * @param player The player connected to the media session.
+ * @param controlDispatcher A {@link ControlDispatcher} that should be used for dispatching
+ * changes to the player.
+ * @param command The command name.
+ * @param extras Optional parameters for the command, may be null.
+ * @param cb A result receiver to which a result may be sent by the command, may be null.
+ * @return Whether the receiver handled the command.
*/
- String[] getCommands();
- /** See {@link MediaSessionCompat.Callback#onCommand(String, Bundle, ResultReceiver)}. */
- void onCommand(Player player, String command, Bundle extras, ResultReceiver cb);
+ boolean onCommand(
+ Player player,
+ ControlDispatcher controlDispatcher,
+ String command,
+ Bundle extras,
+ ResultReceiver cb);
}
- /** Interface to which playback preparation actions are delegated. */
+ /** Interface to which playback preparation and play actions are delegated. */
public interface PlaybackPreparer extends CommandReceiver {
long ACTIONS =
@@ -123,59 +198,36 @@ public final class MediaSessionConnector {
* @return The bitmask of the supported media actions.
*/
long getSupportedPrepareActions();
- /** See {@link MediaSessionCompat.Callback#onPrepare()}. */
- void onPrepare();
- /** See {@link MediaSessionCompat.Callback#onPrepareFromMediaId(String, Bundle)}. */
- void onPrepareFromMediaId(String mediaId, Bundle extras);
- /** See {@link MediaSessionCompat.Callback#onPrepareFromSearch(String, Bundle)}. */
- void onPrepareFromSearch(String query, Bundle extras);
- /** See {@link MediaSessionCompat.Callback#onPrepareFromUri(Uri, Bundle)}. */
- void onPrepareFromUri(Uri uri, Bundle extras);
- }
-
- /** Interface to which playback actions are delegated. */
- public interface PlaybackController extends CommandReceiver {
-
- long ACTIONS =
- PlaybackStateCompat.ACTION_PLAY_PAUSE
- | PlaybackStateCompat.ACTION_PLAY
- | PlaybackStateCompat.ACTION_PAUSE
- | PlaybackStateCompat.ACTION_SEEK_TO
- | PlaybackStateCompat.ACTION_FAST_FORWARD
- | PlaybackStateCompat.ACTION_REWIND
- | PlaybackStateCompat.ACTION_STOP
- | PlaybackStateCompat.ACTION_SET_REPEAT_MODE
- | PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE;
-
/**
- * Returns the actions which are supported by the controller. The supported actions must be a
- * bitmask combined out of {@link PlaybackStateCompat#ACTION_PLAY_PAUSE}, {@link
- * PlaybackStateCompat#ACTION_PLAY}, {@link PlaybackStateCompat#ACTION_PAUSE}, {@link
- * PlaybackStateCompat#ACTION_SEEK_TO}, {@link PlaybackStateCompat#ACTION_FAST_FORWARD}, {@link
- * PlaybackStateCompat#ACTION_REWIND}, {@link PlaybackStateCompat#ACTION_STOP}, {@link
- * PlaybackStateCompat#ACTION_SET_REPEAT_MODE} and {@link
- * PlaybackStateCompat#ACTION_SET_SHUFFLE_MODE}.
+ * See {@link MediaSessionCompat.Callback#onPrepare()}.
*
- * @param player The player.
- * @return The bitmask of the supported media actions.
+ * @param playWhenReady Whether playback should be started after preparation.
*/
- long getSupportedPlaybackActions(@Nullable Player player);
- /** See {@link MediaSessionCompat.Callback#onPlay()}. */
- void onPlay(Player player);
- /** See {@link MediaSessionCompat.Callback#onPause()}. */
- void onPause(Player player);
- /** See {@link MediaSessionCompat.Callback#onSeekTo(long)}. */
- void onSeekTo(Player player, long position);
- /** See {@link MediaSessionCompat.Callback#onFastForward()}. */
- void onFastForward(Player player);
- /** See {@link MediaSessionCompat.Callback#onRewind()}. */
- void onRewind(Player player);
- /** See {@link MediaSessionCompat.Callback#onStop()}. */
- void onStop(Player player);
- /** See {@link MediaSessionCompat.Callback#onSetShuffleMode(int)}. */
- void onSetShuffleMode(Player player, int shuffleMode);
- /** See {@link MediaSessionCompat.Callback#onSetRepeatMode(int)}. */
- void onSetRepeatMode(Player player, int repeatMode);
+ void onPrepare(boolean playWhenReady);
+ /**
+ * See {@link MediaSessionCompat.Callback#onPrepareFromMediaId(String, Bundle)}.
+ *
+ * @param mediaId The media id of the media item to be prepared.
+ * @param playWhenReady Whether playback should be started after preparation.
+ * @param extras A {@link Bundle} of extras passed by the media controller.
+ */
+ void onPrepareFromMediaId(String mediaId, boolean playWhenReady, Bundle extras);
+ /**
+ * See {@link MediaSessionCompat.Callback#onPrepareFromSearch(String, Bundle)}.
+ *
+ * @param query The search query.
+ * @param playWhenReady Whether playback should be started after preparation.
+ * @param extras A {@link Bundle} of extras passed by the media controller.
+ */
+ void onPrepareFromSearch(String query, boolean playWhenReady, Bundle extras);
+ /**
+ * See {@link MediaSessionCompat.Callback#onPrepareFromUri(Uri, Bundle)}.
+ *
+ * @param uri The {@link Uri} of the media item to be prepared.
+ * @param playWhenReady Whether playback should be started after preparation.
+ * @param extras A {@link Bundle} of extras passed by the media controller.
+ */
+ void onPrepareFromUri(Uri uri, boolean playWhenReady, Bundle extras);
}
/**
@@ -195,20 +247,20 @@ public final class MediaSessionConnector {
* PlaybackStateCompat#ACTION_SKIP_TO_NEXT}, {@link
* PlaybackStateCompat#ACTION_SKIP_TO_PREVIOUS}.
*
- * @param player The {@link Player}.
+ * @param player The player connected to the media session.
* @return The bitmask of the supported media actions.
*/
- long getSupportedQueueNavigatorActions(@Nullable Player player);
+ long getSupportedQueueNavigatorActions(Player player);
/**
* Called when the timeline of the player has changed.
*
- * @param player The player of which the timeline has changed.
+ * @param player The player connected to the media session.
*/
void onTimelineChanged(Player player);
/**
* Called when the current window index changed.
*
- * @param player The player of which the current window index of the timeline has changed.
+ * @param player The player connected to the media session.
*/
void onCurrentWindowIndexChanged(Player player);
/**
@@ -223,12 +275,30 @@ public final class MediaSessionConnector {
* @return The id of the active queue item.
*/
long getActiveQueueItemId(@Nullable Player player);
- /** See {@link MediaSessionCompat.Callback#onSkipToPrevious()}. */
- void onSkipToPrevious(Player player);
- /** See {@link MediaSessionCompat.Callback#onSkipToQueueItem(long)}. */
- void onSkipToQueueItem(Player player, long id);
- /** See {@link MediaSessionCompat.Callback#onSkipToNext()}. */
- void onSkipToNext(Player player);
+ /**
+ * See {@link MediaSessionCompat.Callback#onSkipToPrevious()}.
+ *
+ * @param player The player connected to the media session.
+ * @param controlDispatcher A {@link ControlDispatcher} that should be used for dispatching
+ * changes to the player.
+ */
+ void onSkipToPrevious(Player player, ControlDispatcher controlDispatcher);
+ /**
+ * See {@link MediaSessionCompat.Callback#onSkipToQueueItem(long)}.
+ *
+ * @param player The player connected to the media session.
+ * @param controlDispatcher A {@link ControlDispatcher} that should be used for dispatching
+ * changes to the player.
+ */
+ void onSkipToQueueItem(Player player, ControlDispatcher controlDispatcher, long id);
+ /**
+ * See {@link MediaSessionCompat.Callback#onSkipToNext()}.
+ *
+ * @param player The player connected to the media session.
+ * @param controlDispatcher A {@link ControlDispatcher} that should be used for dispatching
+ * changes to the player.
+ */
+ void onSkipToNext(Player player, ControlDispatcher controlDispatcher);
}
/** Handles media session queue edits. */
@@ -253,10 +323,26 @@ public final class MediaSessionConnector {
/** Callback receiving a user rating for the active media item. */
public interface RatingCallback extends CommandReceiver {
- long ACTIONS = PlaybackStateCompat.ACTION_SET_RATING;
-
/** See {@link MediaSessionCompat.Callback#onSetRating(RatingCompat)}. */
void onSetRating(Player player, RatingCompat rating);
+
+ /** See {@link MediaSessionCompat.Callback#onSetRating(RatingCompat, Bundle)}. */
+ void onSetRating(Player player, RatingCompat rating, Bundle extras);
+ }
+
+ /** Handles a media button event. */
+ public interface MediaButtonEventHandler {
+ /**
+ * See {@link MediaSessionCompat.Callback#onMediaButtonEvent(Intent)}.
+ *
+ * @param player The {@link Player}.
+ * @param controlDispatcher A {@link ControlDispatcher} that should be used for dispatching
+ * changes to the player.
+ * @param mediaButtonEvent The {@link Intent}.
+ * @return True if the event was handled, false otherwise.
+ */
+ boolean onMediaButtonEvent(
+ Player player, ControlDispatcher controlDispatcher, Intent mediaButtonEvent);
}
/**
@@ -267,19 +353,24 @@ public final class MediaSessionConnector {
/**
* Called when a custom action provided by this provider is sent to the media session.
*
+ * @param player The player connected to the media session.
+ * @param controlDispatcher A {@link ControlDispatcher} that should be used for dispatching
+ * changes to the player.
* @param action The name of the action which was sent by a media controller.
* @param extras Optional extras sent by a media controller.
*/
- void onCustomAction(String action, Bundle extras);
+ void onCustomAction(
+ Player player, ControlDispatcher controlDispatcher, String action, @Nullable Bundle extras);
/**
* Returns a {@link PlaybackStateCompat.CustomAction} which will be published to the media
* session by the connector or {@code null} if this action should not be published at the given
* player state.
*
+ * @param player The player connected to the media session.
* @return The custom action to be included in the session playback state or {@code null}.
*/
- PlaybackStateCompat.CustomAction getCustomAction();
+ PlaybackStateCompat.CustomAction getCustomAction(Player player);
}
/** Provides a {@link MediaMetadataCompat} for a given player state. */
@@ -287,7 +378,14 @@ public final class MediaSessionConnector {
/**
* Gets the {@link MediaMetadataCompat} to be published to the session.
*
- * @param player The player for which to provide metadata.
+ * An app may need to load metadata resources like artwork bitmaps asynchronously. In such a
+ * case the app should return a {@link MediaMetadataCompat} object that does not contain these
+ * resources as a placeholder. The app should start an asynchronous operation to download the
+ * bitmap and put it into a cache. Finally, the app should call {@link
+ * #invalidateMediaSessionMetadata()}. This causes this callback to be called again and the app
+ * can now return a {@link MediaMetadataCompat} object with all the resources included.
+ *
+ * @param player The player connected to the media session.
* @return The {@link MediaMetadataCompat} to be published to the session.
*/
MediaMetadataCompat getMetadata(Player player);
@@ -296,142 +394,151 @@ public final class MediaSessionConnector {
/** The wrapped {@link MediaSessionCompat}. */
public final MediaSessionCompat mediaSession;
- private @Nullable final MediaMetadataProvider mediaMetadataProvider;
- private final ExoPlayerEventListener exoPlayerEventListener;
- private final MediaSessionCallback mediaSessionCallback;
- private final PlaybackController playbackController;
- private final Map commandMap;
+ private final Looper looper;
+ private final ComponentListener componentListener;
+ private final ArrayList commandReceivers;
+ private final ArrayList customCommandReceivers;
- private Player player;
+ private ControlDispatcher controlDispatcher;
private CustomActionProvider[] customActionProviders;
private Map customActionMap;
- private @Nullable ErrorMessageProvider super ExoPlaybackException> errorMessageProvider;
- private @Nullable Pair customError;
- private PlaybackPreparer playbackPreparer;
- private QueueNavigator queueNavigator;
- private QueueEditor queueEditor;
- private RatingCallback ratingCallback;
+ @Nullable private MediaMetadataProvider mediaMetadataProvider;
+ @Nullable private Player player;
+ @Nullable private ErrorMessageProvider super ExoPlaybackException> errorMessageProvider;
+ @Nullable private Pair customError;
+ @Nullable private Bundle customErrorExtras;
+ @Nullable private PlaybackPreparer playbackPreparer;
+ @Nullable private QueueNavigator queueNavigator;
+ @Nullable private QueueEditor queueEditor;
+ @Nullable private RatingCallback ratingCallback;
+ @Nullable private MediaButtonEventHandler mediaButtonEventHandler;
+
+ private long enabledPlaybackActions;
+ private int rewindMs;
+ private int fastForwardMs;
/**
* Creates an instance.
*
- * Equivalent to {@code MediaSessionConnector(mediaSession, new DefaultPlaybackController())}.
- *
* @param mediaSession The {@link MediaSessionCompat} to connect to.
*/
public MediaSessionConnector(MediaSessionCompat mediaSession) {
- this(mediaSession, null);
- }
-
- /**
- * Creates an instance.
- *
- *
Equivalent to {@code MediaSessionConnector(mediaSession, playbackController, new
- * DefaultMediaMetadataProvider(mediaSession.getController(), null))}.
- *
- * @param mediaSession The {@link MediaSessionCompat} to connect to.
- * @param playbackController A {@link PlaybackController} for handling playback actions.
- */
- public MediaSessionConnector(
- MediaSessionCompat mediaSession, PlaybackController playbackController) {
- this(
- mediaSession,
- playbackController,
- new DefaultMediaMetadataProvider(mediaSession.getController(), null));
- }
-
- /**
- * Creates an instance.
- *
- * @param mediaSession The {@link MediaSessionCompat} to connect to.
- * @param playbackController A {@link PlaybackController} for handling playback actions, or {@code
- * null} if the connector should handle playback actions directly.
- * @param doMaintainMetadata Whether the connector should maintain the metadata of the session.
- * @param metadataExtrasPrefix A string to prefix extra keys which are propagated from the active
- * queue item to the session metadata.
- * @deprecated Use {@link MediaSessionConnector#MediaSessionConnector(MediaSessionCompat,
- * PlaybackController, MediaMetadataProvider)}.
- */
- @Deprecated
- public MediaSessionConnector(
- MediaSessionCompat mediaSession,
- @Nullable PlaybackController playbackController,
- boolean doMaintainMetadata,
- @Nullable String metadataExtrasPrefix) {
- this(
- mediaSession,
- playbackController,
- doMaintainMetadata
- ? new DefaultMediaMetadataProvider(mediaSession.getController(), metadataExtrasPrefix)
- : null);
- }
-
- /**
- * Creates an instance. Must be called on the same thread that is used to construct the player
- * instances passed to {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}.
- *
- * @param mediaSession The {@link MediaSessionCompat} to connect to.
- * @param playbackController A {@link PlaybackController} for handling playback actions, or {@code
- * null} if the connector should handle playback actions directly.
- * @param mediaMetadataProvider A {@link MediaMetadataProvider} for providing a custom metadata
- * object to be published to the media session, or {@code null} if metadata shouldn't be
- * published.
- */
- public MediaSessionConnector(
- MediaSessionCompat mediaSession,
- @Nullable PlaybackController playbackController,
- @Nullable MediaMetadataProvider mediaMetadataProvider) {
this.mediaSession = mediaSession;
- this.playbackController =
- playbackController != null ? playbackController : new DefaultPlaybackController();
- this.mediaMetadataProvider = mediaMetadataProvider;
- mediaSession.setFlags(BASE_MEDIA_SESSION_FLAGS);
- mediaSessionCallback = new MediaSessionCallback();
- exoPlayerEventListener = new ExoPlayerEventListener();
+ looper = Util.getLooper();
+ componentListener = new ComponentListener();
+ commandReceivers = new ArrayList<>();
+ customCommandReceivers = new ArrayList<>();
+ controlDispatcher = new DefaultControlDispatcher();
+ customActionProviders = new CustomActionProvider[0];
customActionMap = Collections.emptyMap();
- commandMap = new HashMap<>();
- registerCommandReceiver(playbackController);
+ mediaMetadataProvider =
+ new DefaultMediaMetadataProvider(
+ mediaSession.getController(), /* metadataExtrasPrefix= */ null);
+ enabledPlaybackActions = DEFAULT_PLAYBACK_ACTIONS;
+ rewindMs = DEFAULT_REWIND_MS;
+ fastForwardMs = DEFAULT_FAST_FORWARD_MS;
+ mediaSession.setFlags(BASE_MEDIA_SESSION_FLAGS);
+ mediaSession.setCallback(componentListener, new Handler(looper));
}
/**
* Sets the player to be connected to the media session. Must be called on the same thread that is
* used to access the player.
*
- *
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 Optional {@link CustomActionProvider}s to publish and handle
- * custom actions.
+ * @param player The player to be connected to the {@code MediaSession}, or {@code null} to
+ * disconnect the current player.
*/
- public void setPlayer(
- Player player,
- @Nullable PlaybackPreparer playbackPreparer,
- CustomActionProvider... customActionProviders) {
+ public void setPlayer(@Nullable Player player) {
+ Assertions.checkArgument(player == null || player.getApplicationLooper() == looper);
if (this.player != null) {
- this.player.removeListener(exoPlayerEventListener);
- mediaSession.setCallback(null);
+ this.player.removeListener(componentListener);
}
- unregisterCommandReceiver(this.playbackPreparer);
-
this.player = player;
- this.playbackPreparer = playbackPreparer;
- registerCommandReceiver(playbackPreparer);
-
- this.customActionProviders =
- (player != null && customActionProviders != null)
- ? customActionProviders
- : new CustomActionProvider[0];
if (player != null) {
- Handler handler = new Handler(Util.getLooper());
- mediaSession.setCallback(mediaSessionCallback, handler);
- player.addListener(exoPlayerEventListener);
+ player.addListener(componentListener);
}
invalidateMediaSessionPlaybackState();
invalidateMediaSessionMetadata();
}
+ /**
+ * Sets the {@link PlaybackPreparer}.
+ *
+ * @param playbackPreparer The {@link PlaybackPreparer}.
+ */
+ public void setPlaybackPreparer(@Nullable PlaybackPreparer playbackPreparer) {
+ if (this.playbackPreparer != playbackPreparer) {
+ unregisterCommandReceiver(this.playbackPreparer);
+ this.playbackPreparer = playbackPreparer;
+ registerCommandReceiver(playbackPreparer);
+ invalidateMediaSessionPlaybackState();
+ }
+ }
+
+ /**
+ * Sets the {@link ControlDispatcher}.
+ *
+ * @param controlDispatcher The {@link ControlDispatcher}, or null to use {@link
+ * DefaultControlDispatcher}.
+ */
+ public void setControlDispatcher(@Nullable ControlDispatcher controlDispatcher) {
+ if (this.controlDispatcher != controlDispatcher) {
+ this.controlDispatcher =
+ controlDispatcher == null ? new DefaultControlDispatcher() : controlDispatcher;
+ }
+ }
+
+ /**
+ * Sets the {@link MediaButtonEventHandler}. Pass {@code null} if the media button event should be
+ * handled by {@link MediaSessionCompat.Callback#onMediaButtonEvent(Intent)}.
+ *
+ * @param mediaButtonEventHandler The {@link MediaButtonEventHandler}, or null to let the event be
+ * handled by {@link MediaSessionCompat.Callback#onMediaButtonEvent(Intent)}.
+ */
+ public void setMediaButtonEventHandler(
+ @Nullable MediaButtonEventHandler mediaButtonEventHandler) {
+ this.mediaButtonEventHandler = mediaButtonEventHandler;
+ }
+
+ /**
+ * Sets the enabled playback actions.
+ *
+ * @param enabledPlaybackActions The enabled playback actions.
+ */
+ public void setEnabledPlaybackActions(@PlaybackActions long enabledPlaybackActions) {
+ enabledPlaybackActions &= ALL_PLAYBACK_ACTIONS;
+ if (this.enabledPlaybackActions != enabledPlaybackActions) {
+ this.enabledPlaybackActions = enabledPlaybackActions;
+ invalidateMediaSessionPlaybackState();
+ }
+ }
+
+ /**
+ * Sets the rewind increment in milliseconds.
+ *
+ * @param rewindMs The rewind increment in milliseconds. A non-positive value will cause the
+ * rewind button to be disabled.
+ */
+ public void setRewindIncrementMs(int rewindMs) {
+ if (this.rewindMs != rewindMs) {
+ this.rewindMs = rewindMs;
+ invalidateMediaSessionPlaybackState();
+ }
+ }
+
+ /**
+ * Sets the fast forward increment in milliseconds.
+ *
+ * @param fastForwardMs The fast forward increment in milliseconds. A non-positive value will
+ * cause the fast forward button to be disabled.
+ */
+ public void setFastForwardIncrementMs(int fastForwardMs) {
+ if (this.fastForwardMs != fastForwardMs) {
+ this.fastForwardMs = fastForwardMs;
+ invalidateMediaSessionPlaybackState();
+ }
+ }
+
/**
* Sets the optional {@link ErrorMessageProvider}.
*
@@ -451,7 +558,7 @@ public final class MediaSessionConnector {
*
* @param queueNavigator The queue navigator.
*/
- public void setQueueNavigator(QueueNavigator queueNavigator) {
+ public void setQueueNavigator(@Nullable QueueNavigator queueNavigator) {
if (this.queueNavigator != queueNavigator) {
unregisterCommandReceiver(this.queueNavigator);
this.queueNavigator = queueNavigator;
@@ -464,7 +571,7 @@ public final class MediaSessionConnector {
*
* @param queueEditor The queue editor.
*/
- public void setQueueEditor(QueueEditor queueEditor) {
+ public void setQueueEditor(@Nullable QueueEditor queueEditor) {
if (this.queueEditor != queueEditor) {
unregisterCommandReceiver(this.queueEditor);
this.queueEditor = queueEditor;
@@ -508,20 +615,65 @@ public final class MediaSessionConnector {
* @param code The error code to report. Ignored when {@code message} is {@code null}.
*/
public void setCustomErrorMessage(@Nullable CharSequence message, int code) {
+ setCustomErrorMessage(message, code, /* extras= */ null);
+ }
+
+ /**
+ * Sets a custom error on the session.
+ *
+ * @param message The error string to report or {@code null} to clear the error.
+ * @param code The error code to report. Ignored when {@code message} is {@code null}.
+ * @param extras Extras to include in reported {@link PlaybackStateCompat}.
+ */
+ public void setCustomErrorMessage(
+ @Nullable CharSequence message, int code, @Nullable Bundle extras) {
customError = (message == null) ? null : new Pair<>(code, message);
+ customErrorExtras = (message == null) ? null : extras;
invalidateMediaSessionPlaybackState();
}
+ /**
+ * Sets custom action providers. The order of the {@link CustomActionProvider}s determines the
+ * order in which the actions are published.
+ *
+ * @param customActionProviders The custom action providers, or null to remove all existing custom
+ * action providers.
+ */
+ public void setCustomActionProviders(@Nullable CustomActionProvider... customActionProviders) {
+ this.customActionProviders =
+ customActionProviders == null ? new CustomActionProvider[0] : customActionProviders;
+ invalidateMediaSessionPlaybackState();
+ }
+
+ /**
+ * Sets a provider of metadata to be published to the media session. Pass {@code null} if no
+ * metadata should be published.
+ *
+ * @param mediaMetadataProvider The provider of metadata to publish, or {@code null} if no
+ * metadata should be published.
+ */
+ public void setMediaMetadataProvider(@Nullable MediaMetadataProvider mediaMetadataProvider) {
+ if (this.mediaMetadataProvider != mediaMetadataProvider) {
+ this.mediaMetadataProvider = mediaMetadataProvider;
+ invalidateMediaSessionMetadata();
+ }
+ }
+
/**
* Updates the metadata of the media session.
*
*
Apps normally only need to call this method when the backing data for a given media item has
* changed and the metadata should be updated immediately.
+ *
+ *
The {@link MediaMetadataCompat} which is published to the session is obtained by calling
+ * {@link MediaMetadataProvider#getMetadata(Player)}.
*/
public final void invalidateMediaSessionMetadata() {
- if (mediaMetadataProvider != null && player != null) {
- mediaSession.setMetadata(mediaMetadataProvider.getMetadata(player));
- }
+ MediaMetadataCompat metadata =
+ mediaMetadataProvider != null && player != null
+ ? mediaMetadataProvider.getMetadata(player)
+ : METADATA_EMPTY;
+ mediaSession.setMetadata(metadata);
}
/**
@@ -532,15 +684,17 @@ public final class MediaSessionConnector {
*/
public final void invalidateMediaSessionPlaybackState() {
PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder();
+ @Nullable Player player = this.player;
if (player == null) {
- builder.setActions(buildPlaybackActions()).setState(PlaybackStateCompat.STATE_NONE, 0, 0, 0);
+ builder.setActions(buildPrepareActions()).setState(PlaybackStateCompat.STATE_NONE, 0, 0, 0);
mediaSession.setPlaybackState(builder.build());
return;
}
Map currentActions = new HashMap<>();
for (CustomActionProvider customActionProvider : customActionProviders) {
- PlaybackStateCompat.CustomAction customAction = customActionProvider.getCustomAction();
+ @Nullable
+ PlaybackStateCompat.CustomAction customAction = customActionProvider.getCustomAction(player);
if (customAction != null) {
currentActions.put(customAction.getAction(), customActionProvider);
builder.addCustomAction(customAction);
@@ -549,6 +703,8 @@ public final class MediaSessionConnector {
customActionMap = Collections.unmodifiableMap(currentActions);
int playbackState = player.getPlaybackState();
+ Bundle extras = new Bundle();
+ @Nullable
ExoPlaybackException playbackError =
playbackState == Player.STATE_IDLE ? player.getPlaybackError() : null;
boolean reportError = playbackError != null || customError != null;
@@ -558,6 +714,9 @@ public final class MediaSessionConnector {
: mapPlaybackState(player.getPlaybackState(), player.getPlayWhenReady());
if (customError != null) {
builder.setErrorMessage(customError.first, customError.second);
+ if (customErrorExtras != null) {
+ extras.putAll(customErrorExtras);
+ }
} else if (playbackError != null && errorMessageProvider != null) {
Pair message = errorMessageProvider.getErrorMessage(playbackError);
builder.setErrorMessage(message.first, message.second);
@@ -566,10 +725,9 @@ public final class MediaSessionConnector {
queueNavigator != null
? queueNavigator.getActiveQueueItemId(player)
: MediaSessionCompat.QueueItem.UNKNOWN_ID;
- Bundle extras = new Bundle();
extras.putFloat(EXTRAS_PITCH, player.getPlaybackParameters().pitch);
builder
- .setActions(buildPlaybackActions())
+ .setActions(buildPrepareActions() | buildPlaybackActions(player))
.setActiveQueueItemId(activeQueueItemId)
.setBufferedPosition(player.getBufferedPosition())
.setState(
@@ -594,34 +752,81 @@ public final class MediaSessionConnector {
}
}
- private void registerCommandReceiver(CommandReceiver commandReceiver) {
- if (commandReceiver != null && commandReceiver.getCommands() != null) {
- for (String command : commandReceiver.getCommands()) {
- commandMap.put(command, commandReceiver);
- }
+ /**
+ * Registers a custom command receiver for responding to commands delivered via {@link
+ * MediaSessionCompat.Callback#onCommand(String, Bundle, ResultReceiver)}.
+ *
+ * Commands are only dispatched to this receiver when a player is connected.
+ *
+ * @param commandReceiver The command receiver to register.
+ */
+ public void registerCustomCommandReceiver(@Nullable CommandReceiver commandReceiver) {
+ if (commandReceiver != null && !customCommandReceivers.contains(commandReceiver)) {
+ customCommandReceivers.add(commandReceiver);
}
}
- private void unregisterCommandReceiver(CommandReceiver commandReceiver) {
- if (commandReceiver != null && commandReceiver.getCommands() != null) {
- for (String command : commandReceiver.getCommands()) {
- commandMap.remove(command);
- }
+ /**
+ * Unregisters a previously registered custom command receiver.
+ *
+ * @param commandReceiver The command receiver to unregister.
+ */
+ public void unregisterCustomCommandReceiver(@Nullable CommandReceiver commandReceiver) {
+ if (commandReceiver != null) {
+ customCommandReceivers.remove(commandReceiver);
}
}
- private long buildPlaybackActions() {
- long actions =
- (PlaybackController.ACTIONS & playbackController.getSupportedPlaybackActions(player));
- if (playbackPreparer != null) {
- actions |= (PlaybackPreparer.ACTIONS & playbackPreparer.getSupportedPrepareActions());
+ private void registerCommandReceiver(@Nullable CommandReceiver commandReceiver) {
+ if (commandReceiver != null && !commandReceivers.contains(commandReceiver)) {
+ commandReceivers.add(commandReceiver);
}
+ }
+
+ private void unregisterCommandReceiver(@Nullable CommandReceiver commandReceiver) {
+ if (commandReceiver != null) {
+ commandReceivers.remove(commandReceiver);
+ }
+ }
+
+ private long buildPrepareActions() {
+ return playbackPreparer == null
+ ? 0
+ : (PlaybackPreparer.ACTIONS & playbackPreparer.getSupportedPrepareActions());
+ }
+
+ private long buildPlaybackActions(Player player) {
+ boolean enableSeeking = false;
+ boolean enableRewind = false;
+ boolean enableFastForward = false;
+ boolean enableSetRating = false;
+ Timeline timeline = player.getCurrentTimeline();
+ if (!timeline.isEmpty() && !player.isPlayingAd()) {
+ enableSeeking = player.isCurrentWindowSeekable();
+ enableRewind = enableSeeking && rewindMs > 0;
+ enableFastForward = enableSeeking && fastForwardMs > 0;
+ enableSetRating = true;
+ }
+
+ long playbackActions = BASE_PLAYBACK_ACTIONS;
+ if (enableSeeking) {
+ playbackActions |= PlaybackStateCompat.ACTION_SEEK_TO;
+ }
+ if (enableFastForward) {
+ playbackActions |= PlaybackStateCompat.ACTION_FAST_FORWARD;
+ }
+ if (enableRewind) {
+ playbackActions |= PlaybackStateCompat.ACTION_REWIND;
+ }
+ playbackActions &= enabledPlaybackActions;
+
+ long actions = playbackActions;
if (queueNavigator != null) {
actions |=
(QueueNavigator.ACTIONS & queueNavigator.getSupportedQueueNavigatorActions(player));
}
- if (ratingCallback != null) {
- actions |= RatingCallback.ACTIONS;
+ if (ratingCallback != null && enableSetRating) {
+ actions |= PlaybackStateCompat.ACTION_SET_RATING;
}
return actions;
}
@@ -633,39 +838,81 @@ public final class MediaSessionConnector {
case Player.STATE_READY:
return playWhenReady ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED;
case Player.STATE_ENDED:
- return PlaybackStateCompat.STATE_PAUSED;
+ return PlaybackStateCompat.STATE_STOPPED;
default:
return PlaybackStateCompat.STATE_NONE;
}
}
+ @EnsuresNonNullIf(result = true, expression = "player")
+ private boolean canDispatchPlaybackAction(long action) {
+ return player != null && (enabledPlaybackActions & action) != 0;
+ }
+
+ @EnsuresNonNullIf(result = true, expression = "playbackPreparer")
private boolean canDispatchToPlaybackPreparer(long action) {
return playbackPreparer != null
- && (playbackPreparer.getSupportedPrepareActions() & PlaybackPreparer.ACTIONS & action) != 0;
- }
-
- private boolean canDispatchToRatingCallback(long action) {
- return ratingCallback != null && (RatingCallback.ACTIONS & action) != 0;
- }
-
- private boolean canDispatchToPlaybackController(long action) {
- return (playbackController.getSupportedPlaybackActions(player)
- & PlaybackController.ACTIONS
- & action)
- != 0;
+ && (playbackPreparer.getSupportedPrepareActions() & action) != 0;
}
+ @EnsuresNonNullIf(
+ result = true,
+ expression = {"player", "queueNavigator"})
private boolean canDispatchToQueueNavigator(long action) {
- return queueNavigator != null
- && (queueNavigator.getSupportedQueueNavigatorActions(player)
- & QueueNavigator.ACTIONS
- & action)
- != 0;
+ return player != null
+ && queueNavigator != null
+ && (queueNavigator.getSupportedQueueNavigatorActions(player) & action) != 0;
+ }
+
+ @EnsuresNonNullIf(
+ result = true,
+ expression = {"player", "ratingCallback"})
+ private boolean canDispatchSetRating() {
+ return player != null && ratingCallback != null;
+ }
+
+ @EnsuresNonNullIf(
+ result = true,
+ expression = {"player", "queueEditor"})
+ private boolean canDispatchQueueEdit() {
+ return player != null && queueEditor != null;
+ }
+
+ @EnsuresNonNullIf(
+ result = true,
+ expression = {"player", "mediaButtonEventHandler"})
+ private boolean canDispatchMediaButtonEvent() {
+ return player != null && mediaButtonEventHandler != null;
+ }
+
+ private void rewind(Player player) {
+ if (player.isCurrentWindowSeekable() && rewindMs > 0) {
+ seekTo(player, player.getCurrentPosition() - rewindMs);
+ }
+ }
+
+ private void fastForward(Player player) {
+ if (player.isCurrentWindowSeekable() && fastForwardMs > 0) {
+ seekTo(player, player.getCurrentPosition() + fastForwardMs);
+ }
+ }
+
+ private void seekTo(Player player, long positionMs) {
+ seekTo(player, player.getCurrentWindowIndex(), positionMs);
+ }
+
+ private void seekTo(Player player, int windowIndex, long positionMs) {
+ long durationMs = player.getDuration();
+ if (durationMs != C.TIME_UNSET) {
+ positionMs = Math.min(positionMs, durationMs);
+ }
+ positionMs = Math.max(positionMs, 0);
+ controlDispatcher.dispatchSeekTo(player, windowIndex, positionMs);
}
/**
- * Provides a default {@link MediaMetadataCompat} with properties and extras propagated from the
- * active queue item to the session metadata.
+ * Provides a default {@link MediaMetadataCompat} with properties and extras taken from the {@link
+ * MediaDescriptionCompat} of the {@link MediaSessionCompat.QueueItem} of the active queue item.
*/
public static final class DefaultMediaMetadataProvider implements MediaMetadataProvider {
@@ -688,7 +935,7 @@ public final class MediaSessionConnector {
@Override
public MediaMetadataCompat getMetadata(Player player) {
if (player.getCurrentTimeline().isEmpty()) {
- return null;
+ return METADATA_EMPTY;
}
MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
if (player.isPlayingAd()) {
@@ -704,10 +951,10 @@ public final class MediaSessionConnector {
MediaSessionCompat.QueueItem queueItem = queue.get(i);
if (queueItem.getQueueId() == activeQueueItemId) {
MediaDescriptionCompat description = queueItem.getDescription();
- Bundle extras = description.getExtras();
+ @Nullable Bundle extras = description.getExtras();
if (extras != null) {
for (String key : extras.keySet()) {
- Object value = extras.get(key);
+ @Nullable Object value = extras.get(key);
if (value instanceof String) {
builder.putString(metadataExtrasPrefix + key, (String) value);
} else if (value instanceof CharSequence) {
@@ -723,39 +970,40 @@ public final class MediaSessionConnector {
}
}
}
- if (description.getTitle() != null) {
- String title = String.valueOf(description.getTitle());
- builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title);
- builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title);
+ @Nullable CharSequence title = description.getTitle();
+ if (title != null) {
+ String titleString = String.valueOf(title);
+ builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, titleString);
+ builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, titleString);
}
- if (description.getSubtitle() != null) {
+ @Nullable CharSequence subtitle = description.getSubtitle();
+ if (subtitle != null) {
builder.putString(
- MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE,
- String.valueOf(description.getSubtitle()));
+ MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, String.valueOf(subtitle));
}
- if (description.getDescription() != null) {
+ @Nullable CharSequence displayDescription = description.getDescription();
+ if (displayDescription != null) {
builder.putString(
MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION,
- String.valueOf(description.getDescription()));
+ String.valueOf(displayDescription));
}
- if (description.getIconBitmap() != null) {
- builder.putBitmap(
- MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, description.getIconBitmap());
+ @Nullable Bitmap iconBitmap = description.getIconBitmap();
+ if (iconBitmap != null) {
+ builder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, iconBitmap);
}
- if (description.getIconUri() != null) {
+ @Nullable Uri iconUri = description.getIconUri();
+ if (iconUri != null) {
builder.putString(
- MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI,
- String.valueOf(description.getIconUri()));
+ MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, String.valueOf(iconUri));
}
- if (description.getMediaId() != null) {
- builder.putString(
- MediaMetadataCompat.METADATA_KEY_MEDIA_ID,
- String.valueOf(description.getMediaId()));
+ @Nullable String mediaId = description.getMediaId();
+ if (mediaId != null) {
+ builder.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, mediaId);
}
- if (description.getMediaUri() != null) {
+ @Nullable Uri mediaUri = description.getMediaUri();
+ if (mediaUri != null) {
builder.putString(
- MediaMetadataCompat.METADATA_KEY_MEDIA_URI,
- String.valueOf(description.getMediaUri()));
+ MediaMetadataCompat.METADATA_KEY_MEDIA_URI, String.valueOf(mediaUri));
}
break;
}
@@ -765,14 +1013,17 @@ public final class MediaSessionConnector {
}
}
- private class ExoPlayerEventListener implements Player.EventListener {
+ private class ComponentListener extends MediaSessionCompat.Callback
+ implements Player.EventListener {
private int currentWindowIndex;
private int currentWindowCount;
+ // Player.EventListener implementation.
+
@Override
- public void onTimelineChanged(
- Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) {
+ public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) {
+ Player player = Assertions.checkNotNull(MediaSessionConnector.this.player);
int windowCount = player.getCurrentTimeline().getWindowCount();
int windowIndex = player.getCurrentWindowIndex();
if (queueNavigator != null) {
@@ -788,7 +1039,7 @@ public final class MediaSessionConnector {
}
@Override
- public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
+ public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
invalidateMediaSessionPlaybackState();
}
@@ -810,10 +1061,12 @@ public final class MediaSessionConnector {
? PlaybackStateCompat.SHUFFLE_MODE_ALL
: PlaybackStateCompat.SHUFFLE_MODE_NONE);
invalidateMediaSessionPlaybackState();
+ invalidateMediaSessionQueue();
}
@Override
public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
+ Player player = Assertions.checkNotNull(MediaSessionConnector.this.player);
if (currentWindowIndex != player.getCurrentWindowIndex()) {
if (queueNavigator != null) {
queueNavigator.onCurrentWindowIndexChanged(player);
@@ -832,193 +1085,238 @@ public final class MediaSessionConnector {
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
invalidateMediaSessionPlaybackState();
}
- }
- private class MediaSessionCallback extends MediaSessionCompat.Callback {
+ // MediaSessionCompat.Callback implementation.
@Override
public void onPlay() {
- if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_PLAY)) {
- playbackController.onPlay(player);
+ if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_PLAY)) {
+ if (player.getPlaybackState() == Player.STATE_IDLE) {
+ if (playbackPreparer != null) {
+ playbackPreparer.onPrepare(/* playWhenReady= */ true);
+ }
+ } else if (player.getPlaybackState() == Player.STATE_ENDED) {
+ controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET);
+ }
+ controlDispatcher.dispatchSetPlayWhenReady(
+ Assertions.checkNotNull(player), /* playWhenReady= */ true);
}
}
@Override
public void onPause() {
- if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_PAUSE)) {
- playbackController.onPause(player);
+ if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_PAUSE)) {
+ controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ false);
}
}
@Override
- public void onSeekTo(long position) {
- if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_SEEK_TO)) {
- playbackController.onSeekTo(player, position);
+ public void onSeekTo(long positionMs) {
+ if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_SEEK_TO)) {
+ seekTo(player, positionMs);
}
}
@Override
public void onFastForward() {
- if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_FAST_FORWARD)) {
- playbackController.onFastForward(player);
+ if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_FAST_FORWARD)) {
+ fastForward(player);
}
}
@Override
public void onRewind() {
- if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_REWIND)) {
- playbackController.onRewind(player);
+ if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_REWIND)) {
+ rewind(player);
}
}
@Override
public void onStop() {
- if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_STOP)) {
- playbackController.onStop(player);
+ if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_STOP)) {
+ controlDispatcher.dispatchStop(player, /* reset= */ true);
}
}
@Override
- public void onSetShuffleMode(int shuffleMode) {
- if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE)) {
- playbackController.onSetShuffleMode(player, shuffleMode);
+ public void onSetShuffleMode(@PlaybackStateCompat.ShuffleMode int shuffleMode) {
+ if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE)) {
+ boolean shuffleModeEnabled;
+ switch (shuffleMode) {
+ case PlaybackStateCompat.SHUFFLE_MODE_ALL:
+ case PlaybackStateCompat.SHUFFLE_MODE_GROUP:
+ shuffleModeEnabled = true;
+ break;
+ case PlaybackStateCompat.SHUFFLE_MODE_NONE:
+ case PlaybackStateCompat.SHUFFLE_MODE_INVALID:
+ default:
+ shuffleModeEnabled = false;
+ break;
+ }
+ controlDispatcher.dispatchSetShuffleModeEnabled(player, shuffleModeEnabled);
}
}
@Override
- public void onSetRepeatMode(int repeatMode) {
- if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_SET_REPEAT_MODE)) {
- playbackController.onSetRepeatMode(player, repeatMode);
+ public void onSetRepeatMode(@PlaybackStateCompat.RepeatMode int mediaSessionRepeatMode) {
+ if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_SET_REPEAT_MODE)) {
+ @RepeatModeUtil.RepeatToggleModes int repeatMode;
+ switch (mediaSessionRepeatMode) {
+ case PlaybackStateCompat.REPEAT_MODE_ALL:
+ case PlaybackStateCompat.REPEAT_MODE_GROUP:
+ repeatMode = Player.REPEAT_MODE_ALL;
+ break;
+ case PlaybackStateCompat.REPEAT_MODE_ONE:
+ repeatMode = Player.REPEAT_MODE_ONE;
+ break;
+ case PlaybackStateCompat.REPEAT_MODE_NONE:
+ case PlaybackStateCompat.REPEAT_MODE_INVALID:
+ default:
+ repeatMode = Player.REPEAT_MODE_OFF;
+ break;
+ }
+ controlDispatcher.dispatchSetRepeatMode(player, repeatMode);
}
}
@Override
public void onSkipToNext() {
if (canDispatchToQueueNavigator(PlaybackStateCompat.ACTION_SKIP_TO_NEXT)) {
- queueNavigator.onSkipToNext(player);
+ queueNavigator.onSkipToNext(player, controlDispatcher);
}
}
@Override
public void onSkipToPrevious() {
if (canDispatchToQueueNavigator(PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS)) {
- queueNavigator.onSkipToPrevious(player);
+ queueNavigator.onSkipToPrevious(player, controlDispatcher);
}
}
@Override
public void onSkipToQueueItem(long id) {
if (canDispatchToQueueNavigator(PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM)) {
- queueNavigator.onSkipToQueueItem(player, id);
+ queueNavigator.onSkipToQueueItem(player, controlDispatcher, id);
}
}
@Override
public void onCustomAction(@NonNull String action, @Nullable Bundle extras) {
- Map actionMap = customActionMap;
- if (actionMap.containsKey(action)) {
- actionMap.get(action).onCustomAction(action, extras);
+ if (player != null && customActionMap.containsKey(action)) {
+ customActionMap.get(action).onCustomAction(player, controlDispatcher, action, extras);
invalidateMediaSessionPlaybackState();
}
}
@Override
public void onCommand(String command, Bundle extras, ResultReceiver cb) {
- CommandReceiver commandReceiver = commandMap.get(command);
- if (commandReceiver != null) {
- commandReceiver.onCommand(player, command, extras, cb);
+ if (player != null) {
+ for (int i = 0; i < commandReceivers.size(); i++) {
+ if (commandReceivers.get(i).onCommand(player, controlDispatcher, command, extras, cb)) {
+ return;
+ }
+ }
+ for (int i = 0; i < customCommandReceivers.size(); i++) {
+ if (customCommandReceivers
+ .get(i)
+ .onCommand(player, controlDispatcher, command, extras, cb)) {
+ return;
+ }
+ }
}
}
@Override
public void onPrepare() {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE)) {
- player.stop();
- player.setPlayWhenReady(false);
- playbackPreparer.onPrepare();
+ playbackPreparer.onPrepare(/* playWhenReady= */ false);
}
}
@Override
public void onPrepareFromMediaId(String mediaId, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID)) {
- player.stop();
- player.setPlayWhenReady(false);
- playbackPreparer.onPrepareFromMediaId(mediaId, extras);
+ playbackPreparer.onPrepareFromMediaId(mediaId, /* playWhenReady= */ false, extras);
}
}
@Override
public void onPrepareFromSearch(String query, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH)) {
- player.stop();
- player.setPlayWhenReady(false);
- playbackPreparer.onPrepareFromSearch(query, extras);
+ playbackPreparer.onPrepareFromSearch(query, /* playWhenReady= */ false, extras);
}
}
@Override
public void onPrepareFromUri(Uri uri, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_URI)) {
- player.stop();
- player.setPlayWhenReady(false);
- playbackPreparer.onPrepareFromUri(uri, extras);
+ playbackPreparer.onPrepareFromUri(uri, /* playWhenReady= */ false, extras);
}
}
@Override
public void onPlayFromMediaId(String mediaId, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID)) {
- player.stop();
- player.setPlayWhenReady(true);
- playbackPreparer.onPrepareFromMediaId(mediaId, extras);
+ playbackPreparer.onPrepareFromMediaId(mediaId, /* playWhenReady= */ true, extras);
}
}
@Override
public void onPlayFromSearch(String query, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH)) {
- player.stop();
- player.setPlayWhenReady(true);
- playbackPreparer.onPrepareFromSearch(query, extras);
+ playbackPreparer.onPrepareFromSearch(query, /* playWhenReady= */ true, extras);
}
}
@Override
public void onPlayFromUri(Uri uri, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_URI)) {
- player.stop();
- player.setPlayWhenReady(true);
- playbackPreparer.onPrepareFromUri(uri, extras);
+ playbackPreparer.onPrepareFromUri(uri, /* playWhenReady= */ true, extras);
}
}
@Override
public void onSetRating(RatingCompat rating) {
- if (canDispatchToRatingCallback(PlaybackStateCompat.ACTION_SET_RATING)) {
+ if (canDispatchSetRating()) {
ratingCallback.onSetRating(player, rating);
}
}
+ @Override
+ public void onSetRating(RatingCompat rating, Bundle extras) {
+ if (canDispatchSetRating()) {
+ ratingCallback.onSetRating(player, rating, extras);
+ }
+ }
+
@Override
public void onAddQueueItem(MediaDescriptionCompat description) {
- if (queueEditor != null) {
+ if (canDispatchQueueEdit()) {
queueEditor.onAddQueueItem(player, description);
}
}
@Override
public void onAddQueueItem(MediaDescriptionCompat description, int index) {
- if (queueEditor != null) {
+ if (canDispatchQueueEdit()) {
queueEditor.onAddQueueItem(player, description, index);
}
}
@Override
public void onRemoveQueueItem(MediaDescriptionCompat description) {
- if (queueEditor != null) {
+ if (canDispatchQueueEdit()) {
queueEditor.onRemoveQueueItem(player, description);
}
}
+
+ @Override
+ public boolean onMediaButtonEvent(Intent mediaButtonEvent) {
+ boolean isHandled =
+ canDispatchMediaButtonEvent()
+ && mediaButtonEventHandler.onMediaButtonEvent(
+ player, controlDispatcher, mediaButtonEvent);
+ return isHandled || super.onMediaButtonEvent(mediaButtonEvent);
+ }
}
}
diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java
index 057f59f62c..5c969dd44d 100644
--- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java
+++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java
@@ -17,18 +17,22 @@ package com.google.android.exoplayer2.ext.mediasession;
import android.content.Context;
import android.os.Bundle;
+import androidx.annotation.Nullable;
import android.support.v4.media.session.PlaybackStateCompat;
+import com.google.android.exoplayer2.ControlDispatcher;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.util.RepeatModeUtil;
-/**
- * Provides a custom action for toggling repeat modes.
- */
+/** Provides a custom action for toggling repeat modes. */
public final class RepeatModeActionProvider implements MediaSessionConnector.CustomActionProvider {
+ /** The default repeat toggle modes. */
+ @RepeatModeUtil.RepeatToggleModes
+ public static final int DEFAULT_REPEAT_TOGGLE_MODES =
+ RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE | RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL;
+
private static final String ACTION_REPEAT_MODE = "ACTION_EXO_REPEAT_MODE";
- private final Player player;
@RepeatModeUtil.RepeatToggleModes
private final int repeatToggleModes;
private final CharSequence repeatAllDescription;
@@ -37,27 +41,23 @@ public final class RepeatModeActionProvider implements MediaSessionConnector.Cus
/**
* Creates a new instance.
- *
- * Equivalent to {@code RepeatModeActionProvider(context, player,
- * MediaSessionConnector.DEFAULT_REPEAT_TOGGLE_MODES)}.
+ *
+ *
Equivalent to {@code RepeatModeActionProvider(context, DEFAULT_REPEAT_TOGGLE_MODES)}.
*
* @param context The context.
- * @param player The player on which to toggle the repeat mode.
*/
- public RepeatModeActionProvider(Context context, Player player) {
- this(context, player, MediaSessionConnector.DEFAULT_REPEAT_TOGGLE_MODES);
+ public RepeatModeActionProvider(Context context) {
+ this(context, DEFAULT_REPEAT_TOGGLE_MODES);
}
/**
* Creates a new instance enabling the given repeat toggle modes.
*
* @param context The context.
- * @param player The player on which to toggle the repeat mode.
* @param repeatToggleModes The toggle modes to enable.
*/
- public RepeatModeActionProvider(Context context, Player player,
- @RepeatModeUtil.RepeatToggleModes int repeatToggleModes) {
- this.player = player;
+ public RepeatModeActionProvider(
+ Context context, @RepeatModeUtil.RepeatToggleModes int repeatToggleModes) {
this.repeatToggleModes = repeatToggleModes;
repeatAllDescription = context.getString(R.string.exo_media_action_repeat_all_description);
repeatOneDescription = context.getString(R.string.exo_media_action_repeat_one_description);
@@ -65,16 +65,17 @@ public final class RepeatModeActionProvider implements MediaSessionConnector.Cus
}
@Override
- public void onCustomAction(String action, Bundle extras) {
+ public void onCustomAction(
+ Player player, ControlDispatcher controlDispatcher, String action, @Nullable Bundle extras) {
int mode = player.getRepeatMode();
int proposedMode = RepeatModeUtil.getNextRepeatMode(mode, repeatToggleModes);
if (mode != proposedMode) {
- player.setRepeatMode(proposedMode);
+ controlDispatcher.dispatchSetRepeatMode(player, proposedMode);
}
}
@Override
- public PlaybackStateCompat.CustomAction getCustomAction() {
+ public PlaybackStateCompat.CustomAction getCustomAction(Player player) {
CharSequence actionLabel;
int iconResourceId;
switch (player.getRepeatMode()) {
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 4f9c553a15..d72f6ffddc 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
@@ -17,12 +17,13 @@ package com.google.android.exoplayer2.ext.mediasession;
import android.os.Bundle;
import android.os.ResultReceiver;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import android.support.v4.media.MediaDescriptionCompat;
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.ControlDispatcher;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
@@ -64,13 +65,6 @@ public final class TimelineQueueEditor
* {@link MediaSessionConnector}.
*/
public interface QueueDataAdapter {
- /**
- * Gets the {@link MediaDescriptionCompat} for a {@code position}.
- *
- * @param position The position in the queue for which to provide a description.
- * @return A {@link MediaDescriptionCompat}.
- */
- MediaDescriptionCompat getMediaDescription(int position);
/**
* Adds a {@link MediaDescriptionCompat} at the given {@code position}.
*
@@ -172,7 +166,7 @@ public final class TimelineQueueEditor
@Override
public void onAddQueueItem(Player player, MediaDescriptionCompat description, int index) {
- MediaSource mediaSource = sourceFactory.createMediaSource(description);
+ @Nullable MediaSource mediaSource = sourceFactory.createMediaSource(description);
if (mediaSource != null) {
queueDataAdapter.add(index, description);
queueMediaSource.addMediaSource(index, mediaSource);
@@ -193,20 +187,23 @@ public final class TimelineQueueEditor
// CommandReceiver implementation.
- @NonNull
@Override
- public String[] getCommands() {
- return new String[] {COMMAND_MOVE_QUEUE_ITEM};
- }
-
- @Override
- public void onCommand(Player player, String command, Bundle extras, ResultReceiver cb) {
+ public boolean onCommand(
+ Player player,
+ ControlDispatcher controlDispatcher,
+ String command,
+ Bundle extras,
+ ResultReceiver cb) {
+ if (!COMMAND_MOVE_QUEUE_ITEM.equals(command)) {
+ return false;
+ }
int from = extras.getInt(EXTRA_FROM_INDEX, C.INDEX_UNSET);
int to = extras.getInt(EXTRA_TO_INDEX, C.INDEX_UNSET);
if (from != C.INDEX_UNSET && to != C.INDEX_UNSET) {
queueDataAdapter.move(from, to);
queueMediaSource.moveMediaSource(from, to);
}
+ return true;
}
}
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 6671add7e5..b89a6f4eab 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
@@ -17,17 +17,18 @@ package com.google.android.exoplayer2.ext.mediasession;
import android.os.Bundle;
import android.os.ResultReceiver;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ControlDispatcher;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
-import com.google.android.exoplayer2.util.Util;
+import com.google.android.exoplayer2.util.Assertions;
+import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
-import java.util.List;
/**
* An abstract implementation of the {@link MediaSessionConnector.QueueNavigator} that maps the
@@ -39,7 +40,8 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu
public static final int DEFAULT_MAX_QUEUE_SIZE = 10;
private final MediaSessionCompat mediaSession;
- protected final int maxQueueSize;
+ private final Timeline.Window window;
+ private final int maxQueueSize;
private long activeQueueItemId;
@@ -65,14 +67,21 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu
* @param maxQueueSize The maximum queue size.
*/
public TimelineQueueNavigator(MediaSessionCompat mediaSession, int maxQueueSize) {
+ Assertions.checkState(maxQueueSize > 0);
this.mediaSession = mediaSession;
this.maxQueueSize = maxQueueSize;
activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID;
+ window = new Timeline.Window();
}
/**
* Gets the {@link MediaDescriptionCompat} for a given timeline window index.
*
+ *
Often artworks and icons need to be loaded asynchronously. In such a case, return a {@link
+ * MediaDescriptionCompat} without the images, load your images asynchronously off the main thread
+ * and then call {@link MediaSessionConnector#invalidateMediaSessionQueue()} to make the connector
+ * update the queue by calling this method again.
+ *
* @param player The current player.
* @param windowIndex The timeline window index for which to provide a description.
* @return A {@link MediaDescriptionCompat}.
@@ -81,25 +90,28 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu
@Override
public long getSupportedQueueNavigatorActions(Player player) {
- if (player == null || player.getCurrentTimeline().getWindowCount() < 2) {
- return 0;
- }
- if (player.getRepeatMode() != Player.REPEAT_MODE_OFF) {
- return PlaybackStateCompat.ACTION_SKIP_TO_NEXT | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
- | PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM;
+ boolean enableSkipTo = false;
+ boolean enablePrevious = false;
+ boolean enableNext = false;
+ Timeline timeline = player.getCurrentTimeline();
+ if (!timeline.isEmpty() && !player.isPlayingAd()) {
+ timeline.getWindow(player.getCurrentWindowIndex(), window);
+ enableSkipTo = timeline.getWindowCount() > 1;
+ enablePrevious = window.isSeekable || !window.isDynamic || player.hasPrevious();
+ enableNext = window.isDynamic || player.hasNext();
}
- int currentWindowIndex = player.getCurrentWindowIndex();
- long actions;
- if (currentWindowIndex == 0) {
- actions = PlaybackStateCompat.ACTION_SKIP_TO_NEXT;
- } else if (currentWindowIndex == player.getCurrentTimeline().getWindowCount() - 1) {
- actions = PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;
- } else {
- actions = PlaybackStateCompat.ACTION_SKIP_TO_NEXT
- | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;
+ long actions = 0;
+ if (enableSkipTo) {
+ actions |= PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM;
}
- return actions | PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM;
+ if (enablePrevious) {
+ actions |= PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;
+ }
+ if (enableNext) {
+ actions |= PlaybackStateCompat.ACTION_SKIP_TO_NEXT;
+ }
+ return actions;
}
@Override
@@ -123,74 +135,109 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu
}
@Override
- public void onSkipToPrevious(Player player) {
+ public void onSkipToPrevious(Player player, ControlDispatcher controlDispatcher) {
Timeline timeline = player.getCurrentTimeline();
- if (timeline.isEmpty()) {
+ if (timeline.isEmpty() || player.isPlayingAd()) {
return;
}
+ int windowIndex = player.getCurrentWindowIndex();
+ timeline.getWindow(windowIndex, window);
int previousWindowIndex = player.getPreviousWindowIndex();
- if (player.getCurrentPosition() > MAX_POSITION_FOR_SEEK_TO_PREVIOUS
- || previousWindowIndex == C.INDEX_UNSET) {
- player.seekTo(0);
+ if (previousWindowIndex != C.INDEX_UNSET
+ && (player.getCurrentPosition() <= MAX_POSITION_FOR_SEEK_TO_PREVIOUS
+ || (window.isDynamic && !window.isSeekable))) {
+ controlDispatcher.dispatchSeekTo(player, previousWindowIndex, C.TIME_UNSET);
} else {
- player.seekTo(previousWindowIndex, C.TIME_UNSET);
+ controlDispatcher.dispatchSeekTo(player, windowIndex, 0);
}
}
@Override
- public void onSkipToQueueItem(Player player, long id) {
+ public void onSkipToQueueItem(Player player, ControlDispatcher controlDispatcher, long id) {
Timeline timeline = player.getCurrentTimeline();
- if (timeline.isEmpty()) {
+ if (timeline.isEmpty() || player.isPlayingAd()) {
return;
}
int windowIndex = (int) id;
if (0 <= windowIndex && windowIndex < timeline.getWindowCount()) {
- player.seekTo(windowIndex, C.TIME_UNSET);
+ controlDispatcher.dispatchSeekTo(player, windowIndex, C.TIME_UNSET);
}
}
@Override
- public void onSkipToNext(Player player) {
+ public void onSkipToNext(Player player, ControlDispatcher controlDispatcher) {
Timeline timeline = player.getCurrentTimeline();
- if (timeline.isEmpty()) {
+ if (timeline.isEmpty() || player.isPlayingAd()) {
return;
}
+ int windowIndex = player.getCurrentWindowIndex();
int nextWindowIndex = player.getNextWindowIndex();
if (nextWindowIndex != C.INDEX_UNSET) {
- player.seekTo(nextWindowIndex, C.TIME_UNSET);
+ controlDispatcher.dispatchSeekTo(player, nextWindowIndex, C.TIME_UNSET);
+ } else if (timeline.getWindow(windowIndex, window).isDynamic) {
+ controlDispatcher.dispatchSeekTo(player, windowIndex, C.TIME_UNSET);
}
}
// CommandReceiver implementation.
@Override
- public String[] getCommands() {
- return null;
+ public boolean onCommand(
+ Player player,
+ ControlDispatcher controlDispatcher,
+ String command,
+ Bundle extras,
+ ResultReceiver cb) {
+ return false;
}
- @Override
- public void onCommand(Player player, String command, Bundle extras, ResultReceiver cb) {
- // Do nothing.
- }
+ // Helper methods.
private void publishFloatingQueueWindow(Player player) {
- if (player.getCurrentTimeline().isEmpty()) {
+ Timeline timeline = player.getCurrentTimeline();
+ if (timeline.isEmpty()) {
mediaSession.setQueue(Collections.emptyList());
activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID;
return;
}
- int windowCount = player.getCurrentTimeline().getWindowCount();
+ ArrayDeque queue = new ArrayDeque<>();
+ int queueSize = Math.min(maxQueueSize, timeline.getWindowCount());
+
+ // Add the active queue item.
int currentWindowIndex = player.getCurrentWindowIndex();
- int queueSize = Math.min(maxQueueSize, windowCount);
- int startIndex = Util.constrainValue(currentWindowIndex - ((queueSize - 1) / 2), 0,
- windowCount - queueSize);
- List queue = new ArrayList<>();
- for (int i = startIndex; i < startIndex + queueSize; i++) {
- queue.add(new MediaSessionCompat.QueueItem(getMediaDescription(player, i), i));
+ queue.add(
+ new MediaSessionCompat.QueueItem(
+ getMediaDescription(player, currentWindowIndex), currentWindowIndex));
+
+ // Fill queue alternating with next and/or previous queue items.
+ int firstWindowIndex = currentWindowIndex;
+ int lastWindowIndex = currentWindowIndex;
+ boolean shuffleModeEnabled = player.getShuffleModeEnabled();
+ while ((firstWindowIndex != C.INDEX_UNSET || lastWindowIndex != C.INDEX_UNSET)
+ && queue.size() < queueSize) {
+ // Begin with next to have a longer tail than head if an even sized queue needs to be trimmed.
+ if (lastWindowIndex != C.INDEX_UNSET) {
+ lastWindowIndex =
+ timeline.getNextWindowIndex(
+ lastWindowIndex, Player.REPEAT_MODE_OFF, shuffleModeEnabled);
+ if (lastWindowIndex != C.INDEX_UNSET) {
+ queue.add(
+ new MediaSessionCompat.QueueItem(
+ getMediaDescription(player, lastWindowIndex), lastWindowIndex));
+ }
+ }
+ if (firstWindowIndex != C.INDEX_UNSET && queue.size() < queueSize) {
+ firstWindowIndex =
+ timeline.getPreviousWindowIndex(
+ firstWindowIndex, Player.REPEAT_MODE_OFF, shuffleModeEnabled);
+ if (firstWindowIndex != C.INDEX_UNSET) {
+ queue.addFirst(
+ new MediaSessionCompat.QueueItem(
+ getMediaDescription(player, firstWindowIndex), firstWindowIndex));
+ }
+ }
}
- mediaSession.setQueue(queue);
+ mediaSession.setQueue(new ArrayList<>(queue));
activeQueueItemId = currentWindowIndex;
}
-
}
-
diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/package-info.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/package-info.java
new file mode 100644
index 0000000000..65c0ce080e
--- /dev/null
+++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+@NonNullApi
+package com.google.android.exoplayer2.ext.mediasession;
+
+import com.google.android.exoplayer2.util.NonNullApi;
diff --git a/extensions/okhttp/README.md b/extensions/okhttp/README.md
index 73297b54a9..2f9893fe3b 100644
--- a/extensions/okhttp/README.md
+++ b/extensions/okhttp/README.md
@@ -3,7 +3,7 @@
The OkHttp extension is an [HttpDataSource][] implementation using Square's
[OkHttp][].
-[HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/upstream/HttpDataSource.html
+[HttpDataSource]: https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/upstream/HttpDataSource.html
[OkHttp]: https://square.github.io/okhttp/
## License note ##
@@ -61,4 +61,4 @@ respectively.
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.okhttp.*`
belong to this module.
-[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
+[Javadoc]: https://exoplayer.dev/doc/reference/index.html
diff --git a/extensions/okhttp/build.gradle b/extensions/okhttp/build.gradle
index 4e6b11c495..68bd422185 100644
--- a/extensions/okhttp/build.gradle
+++ b/extensions/okhttp/build.gradle
@@ -16,7 +16,6 @@ apply plugin: 'com.android.library'
android {
compileSdkVersion project.ext.compileSdkVersion
- buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@@ -28,13 +27,15 @@ android {
targetSdkVersion project.ext.targetSdkVersion
consumerProguardFiles 'proguard-rules.txt'
}
+
+ testOptions.unitTests.includeAndroidResources = true
}
dependencies {
implementation project(modulePrefix + 'library-core')
- implementation 'com.android.support:support-annotations:' + supportLibraryVersion
+ implementation 'androidx.annotation:annotation:1.1.0'
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
- api 'com.squareup.okhttp3:okhttp:3.11.0'
+ api 'com.squareup.okhttp3:okhttp:3.12.1'
}
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 ba5640c4e0..ec05c52f44 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
@@ -18,15 +18,17 @@ package com.google.android.exoplayer2.ext.okhttp;
import static com.google.android.exoplayer2.util.Util.castNonNull;
import android.net.Uri;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
+import com.google.android.exoplayer2.metadata.icy.IcyHeaders;
import com.google.android.exoplayer2.upstream.BaseDataSource;
import com.google.android.exoplayer2.upstream.DataSourceException;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Predicate;
+import com.google.android.exoplayer2.util.Util;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
@@ -55,14 +57,14 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
private final Call.Factory callFactory;
private final RequestProperties requestProperties;
- private final @Nullable String userAgent;
- private final @Nullable Predicate contentTypePredicate;
- private final @Nullable CacheControl cacheControl;
- private final @Nullable RequestProperties defaultRequestProperties;
+ @Nullable private final String userAgent;
+ @Nullable private final CacheControl cacheControl;
+ @Nullable private final RequestProperties defaultRequestProperties;
- private @Nullable DataSpec dataSpec;
- private @Nullable Response response;
- private @Nullable InputStream responseByteStream;
+ @Nullable private Predicate contentTypePredicate;
+ @Nullable private DataSpec dataSpec;
+ @Nullable private Response response;
+ @Nullable private InputStream responseByteStream;
private boolean opened;
private long bytesToSkip;
@@ -71,6 +73,36 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
private long bytesSkipped;
private long bytesRead;
+ /**
+ * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
+ * by the source.
+ * @param userAgent An optional User-Agent string.
+ */
+ public OkHttpDataSource(Call.Factory callFactory, @Nullable String userAgent) {
+ this(callFactory, userAgent, /* cacheControl= */ null, /* defaultRequestProperties= */ null);
+ }
+
+ /**
+ * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
+ * by the source.
+ * @param userAgent An optional User-Agent string.
+ * @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header.
+ * @param defaultRequestProperties Optional default {@link RequestProperties} to be sent to the
+ * server as HTTP headers on every request.
+ */
+ public OkHttpDataSource(
+ Call.Factory callFactory,
+ @Nullable String userAgent,
+ @Nullable CacheControl cacheControl,
+ @Nullable RequestProperties defaultRequestProperties) {
+ super(/* isNetwork= */ true);
+ this.callFactory = Assertions.checkNotNull(callFactory);
+ this.userAgent = userAgent;
+ this.cacheControl = cacheControl;
+ this.defaultRequestProperties = defaultRequestProperties;
+ this.requestProperties = new RequestProperties();
+ }
+
/**
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
* by the source.
@@ -78,7 +110,10 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
* predicate then a {@link InvalidContentTypeException} is thrown from {@link
* #open(DataSpec)}.
+ * @deprecated Use {@link #OkHttpDataSource(Call.Factory, String)} and {@link
+ * #setContentTypePredicate(Predicate)}.
*/
+ @Deprecated
public OkHttpDataSource(
Call.Factory callFactory,
@Nullable String userAgent,
@@ -99,9 +134,12 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
* predicate then a {@link InvalidContentTypeException} is thrown from {@link
* #open(DataSpec)}.
* @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header.
- * @param defaultRequestProperties The optional default {@link RequestProperties} to be sent to
- * the server as HTTP headers on every request.
+ * @param defaultRequestProperties Optional default {@link RequestProperties} to be sent to the
+ * server as HTTP headers on every request.
+ * @deprecated Use {@link #OkHttpDataSource(Call.Factory, String, CacheControl,
+ * RequestProperties)} and {@link #setContentTypePredicate(Predicate)}.
*/
+ @Deprecated
public OkHttpDataSource(
Call.Factory callFactory,
@Nullable String userAgent,
@@ -117,8 +155,20 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
this.requestProperties = new RequestProperties();
}
+ /**
+ * Sets a content type {@link Predicate}. If a content type is rejected by the predicate then a
+ * {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link #open(DataSpec)}.
+ *
+ * @param contentTypePredicate The content type {@link Predicate}, or {@code null} to clear a
+ * predicate that was previously set.
+ */
+ public void setContentTypePredicate(@Nullable Predicate contentTypePredicate) {
+ this.contentTypePredicate = contentTypePredicate;
+ }
+
@Override
- public @Nullable Uri getUri() {
+ @Nullable
+ public Uri getUri() {
return response == null ? null : Uri.parse(response.request().url().toString());
}
@@ -171,8 +221,8 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
if (!response.isSuccessful()) {
Map> headers = response.headers().toMultimap();
closeConnectionQuietly();
- InvalidResponseCodeException exception = new InvalidResponseCodeException(
- responseCode, headers, dataSpec);
+ InvalidResponseCodeException exception =
+ new InvalidResponseCodeException(responseCode, response.message(), headers, dataSpec);
if (responseCode == 416) {
exception.initCause(new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE));
}
@@ -262,7 +312,6 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
private Request makeRequest(DataSpec dataSpec) throws HttpDataSourceException {
long position = dataSpec.position;
long length = dataSpec.length;
- boolean allowGzip = dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP);
HttpUrl url = HttpUrl.parse(dataSpec.uri.toString());
if (url == null) {
@@ -292,16 +341,20 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
if (userAgent != null) {
builder.addHeader("User-Agent", userAgent);
}
-
- if (!allowGzip) {
+ if (!dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP)) {
builder.addHeader("Accept-Encoding", "identity");
}
+ if (dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_ICY_METADATA)) {
+ builder.addHeader(
+ IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_NAME,
+ IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_VALUE);
+ }
RequestBody requestBody = null;
if (dataSpec.httpBody != null) {
requestBody = RequestBody.create(null, dataSpec.httpBody);
} else if (dataSpec.httpMethod == DataSpec.HTTP_METHOD_POST) {
// OkHttp requires a non-null body for POST requests.
- requestBody = RequestBody.create(null, new byte[0]);
+ requestBody = RequestBody.create(null, Util.EMPTY_BYTE_ARRAY);
}
builder.method(dataSpec.getHttpMethodString(), requestBody);
return builder.build();
diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java
index 09f4e0b61a..f3d74f9233 100644
--- a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java
+++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java
@@ -15,7 +15,7 @@
*/
package com.google.android.exoplayer2.ext.okhttp;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;
import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
@@ -29,9 +29,9 @@ import okhttp3.Call;
public final class OkHttpDataSourceFactory extends BaseFactory {
private final Call.Factory callFactory;
- private final @Nullable String userAgent;
- private final @Nullable TransferListener listener;
- private final @Nullable CacheControl cacheControl;
+ @Nullable private final String userAgent;
+ @Nullable private final TransferListener listener;
+ @Nullable private final CacheControl cacheControl;
/**
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
@@ -89,7 +89,6 @@ public final class OkHttpDataSourceFactory extends BaseFactory {
new OkHttpDataSource(
callFactory,
userAgent,
- /* contentTypePredicate= */ null,
cacheControl,
defaultRequestProperties);
if (listener != null) {
diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/package-info.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/package-info.java
new file mode 100644
index 0000000000..54eb4d5967
--- /dev/null
+++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+@NonNullApi
+package com.google.android.exoplayer2.ext.okhttp;
+
+import com.google.android.exoplayer2.util.NonNullApi;
diff --git a/extensions/opus/README.md b/extensions/opus/README.md
index 15c3e5413d..95c6807275 100644
--- a/extensions/opus/README.md
+++ b/extensions/opus/README.md
@@ -98,4 +98,4 @@ player, then implement your own logic to use the renderer for a given track.
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.opus.*`
belong to this module.
-[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
+[Javadoc]: https://exoplayer.dev/doc/reference/index.html
diff --git a/extensions/opus/build.gradle b/extensions/opus/build.gradle
index cb12442de8..7b621a8df9 100644
--- a/extensions/opus/build.gradle
+++ b/extensions/opus/build.gradle
@@ -16,7 +16,6 @@ apply plugin: 'com.android.library'
android {
compileSdkVersion project.ext.compileSdkVersion
- buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@@ -34,11 +33,17 @@ android {
jniLibs.srcDir 'src/main/libs'
jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio.
}
+
+ testOptions.unitTests.includeAndroidResources = true
}
dependencies {
implementation project(modulePrefix + 'library-core')
- androidTestImplementation 'androidx.test:runner:' + testRunnerVersion
+ implementation 'androidx.annotation:annotation:1.1.0'
+ testImplementation project(modulePrefix + 'testutils')
+ testImplementation 'org.robolectric:robolectric:' + robolectricVersion
+ androidTestImplementation 'androidx.test:runner:' + androidXTestVersion
+ androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion
}
ext {
diff --git a/extensions/opus/src/androidTest/AndroidManifest.xml b/extensions/opus/src/androidTest/AndroidManifest.xml
index 5ba0f3c0f4..7f775f4d32 100644
--- a/extensions/opus/src/androidTest/AndroidManifest.xml
+++ b/extensions/opus/src/androidTest/AndroidManifest.xml
@@ -18,6 +18,9 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.google.android.exoplayer2.ext.opus.test">
+
+
+
diff --git a/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java b/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java
index c457514c87..382ee38e06 100644
--- a/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java
+++ b/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java
@@ -15,21 +15,21 @@
*/
package com.google.android.exoplayer2.ext.opus;
-import static androidx.test.InstrumentationRegistry.getContext;
import static org.junit.Assert.fail;
import android.content.Context;
import android.net.Uri;
import android.os.Looper;
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
-import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import org.junit.Before;
@@ -56,7 +56,7 @@ public class OpusPlaybackTest {
private void playUri(String uri) throws Exception {
TestPlaybackRunnable testPlaybackRunnable =
- new TestPlaybackRunnable(Uri.parse(uri), getContext());
+ new TestPlaybackRunnable(Uri.parse(uri), ApplicationProvider.getApplicationContext());
Thread thread = new Thread(testPlaybackRunnable);
thread.start();
thread.join();
@@ -82,13 +82,13 @@ public class OpusPlaybackTest {
public void run() {
Looper.prepare();
LibopusAudioRenderer audioRenderer = new LibopusAudioRenderer();
- DefaultTrackSelector trackSelector = new DefaultTrackSelector();
- player = ExoPlayerFactory.newInstance(new Renderer[] {audioRenderer}, trackSelector);
+ DefaultTrackSelector trackSelector = new DefaultTrackSelector(context);
+ player = ExoPlayerFactory.newInstance(context, new Renderer[] {audioRenderer}, trackSelector);
player.addListener(this);
MediaSource mediaSource =
- new ExtractorMediaSource.Factory(
- new DefaultDataSourceFactory(context, "ExoPlayerExtOpusTest"))
- .setExtractorsFactory(MatroskaExtractor.FACTORY)
+ new ProgressiveMediaSource.Factory(
+ new DefaultDataSourceFactory(context, "ExoPlayerExtOpusTest"),
+ MatroskaExtractor.FACTORY)
.createMediaSource(uri);
player.prepare(mediaSource);
player.setPlayWhenReady(true);
@@ -101,7 +101,7 @@ public class OpusPlaybackTest {
}
@Override
- public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
+ public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
if (playbackState == Player.STATE_ENDED
|| (playbackState == Player.STATE_IDLE && playbackException != null)) {
player.release();
diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java
index 57937b4282..2e9638c447 100644
--- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java
+++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java
@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.ext.opus;
import android.os.Handler;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.audio.AudioProcessor;
@@ -25,20 +26,20 @@ import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.util.MimeTypes;
-/**
- * Decodes and renders audio using the native Opus decoder.
- */
-public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {
+/** Decodes and renders audio using the native Opus decoder. */
+public class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {
/** The number of input and output buffers. */
private static final int NUM_BUFFERS = 16;
/** The default input buffer size. */
private static final int DEFAULT_INPUT_BUFFER_SIZE = 960 * 6;
- private OpusDecoder decoder;
+ @Nullable private OpusDecoder decoder;
+ private int channelCount;
+ private int sampleRate;
public LibopusAudioRenderer() {
- this(null, null);
+ this(/* eventHandler= */ null, /* eventListener= */ null);
}
/**
@@ -47,7 +48,9 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {
* @param eventListener A listener of events. May be null if delivery of events is not required.
* @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
*/
- public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
+ public LibopusAudioRenderer(
+ @Nullable Handler eventHandler,
+ @Nullable AudioRendererEventListener eventListener,
AudioProcessor... audioProcessors) {
super(eventHandler, eventListener, audioProcessors);
}
@@ -65,22 +68,30 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {
* has obtained the keys necessary to decrypt encrypted regions of the media.
* @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
*/
- public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
- DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys,
+ public LibopusAudioRenderer(
+ @Nullable Handler eventHandler,
+ @Nullable AudioRendererEventListener eventListener,
+ @Nullable DrmSessionManager drmSessionManager,
+ boolean playClearSamplesWithoutKeys,
AudioProcessor... audioProcessors) {
super(eventHandler, eventListener, null, drmSessionManager, playClearSamplesWithoutKeys,
audioProcessors);
}
@Override
- protected int supportsFormatInternal(DrmSessionManager drmSessionManager,
- Format format) {
+ protected int supportsFormatInternal(
+ @Nullable DrmSessionManager drmSessionManager, Format format) {
+ boolean drmIsSupported =
+ format.drmInitData == null
+ || OpusLibrary.matchesExpectedExoMediaCryptoType(format.exoMediaCryptoType)
+ || (format.exoMediaCryptoType == null
+ && supportsFormatDrm(drmSessionManager, format.drmInitData));
if (!OpusLibrary.isAvailable()
|| !MimeTypes.AUDIO_OPUS.equalsIgnoreCase(format.sampleMimeType)) {
return FORMAT_UNSUPPORTED_TYPE;
- } else if (!supportsOutputEncoding(C.ENCODING_PCM_16BIT)) {
+ } else if (!supportsOutput(format.channelCount, C.ENCODING_PCM_16BIT)) {
return FORMAT_UNSUPPORTED_SUBTYPE;
- } else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {
+ } else if (!drmIsSupported) {
return FORMAT_UNSUPPORTED_DRM;
} else {
return FORMAT_HANDLED;
@@ -88,7 +99,7 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {
}
@Override
- protected OpusDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto)
+ protected OpusDecoder createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto)
throws OpusDecoderException {
int initialInputBufferSize =
format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE;
@@ -99,14 +110,25 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {
initialInputBufferSize,
format.initializationData,
mediaCrypto);
+ channelCount = decoder.getChannelCount();
+ sampleRate = decoder.getSampleRate();
return decoder;
}
@Override
protected Format getOutputFormat() {
- return Format.createAudioSampleFormat(null, MimeTypes.AUDIO_RAW, null, Format.NO_VALUE,
- Format.NO_VALUE, decoder.getChannelCount(), decoder.getSampleRate(), C.ENCODING_PCM_16BIT,
- null, null, 0, null);
+ return Format.createAudioSampleFormat(
+ /* id= */ null,
+ MimeTypes.AUDIO_RAW,
+ /* codecs= */ null,
+ Format.NO_VALUE,
+ Format.NO_VALUE,
+ channelCount,
+ sampleRate,
+ C.ENCODING_PCM_16BIT,
+ /* initializationData= */ null,
+ /* drmInitData= */ null,
+ /* selectionFlags= */ 0,
+ /* language= */ null);
}
-
}
diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java
index f8ec477b88..d93036113c 100644
--- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java
+++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.ext.opus;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.decoder.CryptoInfo;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
@@ -43,7 +44,7 @@ import java.util.List;
private static final int DECODE_ERROR = -1;
private static final int DRM_ERROR = -2;
- private final ExoMediaCrypto exoMediaCrypto;
+ @Nullable private final ExoMediaCrypto exoMediaCrypto;
private final int channelCount;
private final int headerSkipSamples;
@@ -65,8 +66,13 @@ import java.util.List;
* content. Maybe null and can be ignored if decoder does not handle encrypted content.
* @throws OpusDecoderException Thrown if an exception occurs when initializing the decoder.
*/
- public OpusDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize,
- List initializationData, ExoMediaCrypto exoMediaCrypto) throws OpusDecoderException {
+ public OpusDecoder(
+ int numInputBuffers,
+ int numOutputBuffers,
+ int initialInputBufferSize,
+ List initializationData,
+ @Nullable ExoMediaCrypto exoMediaCrypto)
+ throws OpusDecoderException {
super(new DecoderInputBuffer[numInputBuffers], new SimpleOutputBuffer[numOutputBuffers]);
if (!OpusLibrary.isAvailable()) {
throw new OpusDecoderException("Failed to load decoder native libraries.");
@@ -150,6 +156,7 @@ import java.util.List;
}
@Override
+ @Nullable
protected OpusDecoderException decode(
DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) {
if (reset) {
@@ -230,10 +237,22 @@ import java.util.List;
int gain, byte[] streamMap);
private native int opusDecode(long decoder, long timeUs, ByteBuffer inputBuffer, int inputSize,
SimpleOutputBuffer outputBuffer);
- private native int opusSecureDecode(long decoder, long timeUs, ByteBuffer inputBuffer,
- int inputSize, SimpleOutputBuffer outputBuffer, int sampleRate,
- ExoMediaCrypto mediaCrypto, int inputMode, byte[] key, byte[] iv,
- int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData);
+
+ private native int opusSecureDecode(
+ long decoder,
+ long timeUs,
+ ByteBuffer inputBuffer,
+ int inputSize,
+ SimpleOutputBuffer outputBuffer,
+ int sampleRate,
+ @Nullable ExoMediaCrypto mediaCrypto,
+ int inputMode,
+ byte[] key,
+ byte[] iv,
+ int numSubSamples,
+ int[] numBytesOfClearData,
+ int[] numBytesOfEncryptedData);
+
private native void opusClose(long decoder);
private native void opusReset(long decoder);
private native int opusGetErrorCode(long decoder);
diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java
index 4cb3ce3190..d09d69bf03 100644
--- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java
+++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java
@@ -15,8 +15,11 @@
*/
package com.google.android.exoplayer2.ext.opus;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
+import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.util.LibraryLoader;
+import com.google.android.exoplayer2.util.Util;
/**
* Configures and queries the underlying native library.
@@ -27,7 +30,8 @@ public final class OpusLibrary {
ExoPlayerLibraryInfo.registerModule("goog.exo.opus");
}
- private static final LibraryLoader LOADER = new LibraryLoader("opusJNI");
+ private static final LibraryLoader LOADER = new LibraryLoader("opusV2JNI");
+ @Nullable private static Class extends ExoMediaCrypto> exoMediaCryptoType;
private OpusLibrary() {}
@@ -36,10 +40,14 @@ public final class OpusLibrary {
* it must do so before calling any other method defined by this class, and before instantiating a
* {@link LibopusAudioRenderer} instance.
*
+ * @param exoMediaCryptoType The {@link ExoMediaCrypto} type expected for decoding protected
+ * content.
* @param libraries The names of the Opus native libraries.
*/
- public static void setLibraries(String... libraries) {
+ public static void setLibraries(
+ Class extends ExoMediaCrypto> exoMediaCryptoType, String... libraries) {
LOADER.setLibraries(libraries);
+ OpusLibrary.exoMediaCryptoType = exoMediaCryptoType;
}
/**
@@ -49,13 +57,21 @@ public final class OpusLibrary {
return LOADER.isAvailable();
}
- /**
- * Returns the version of the underlying library if available, or null otherwise.
- */
+ /** Returns the version of the underlying library if available, or null otherwise. */
+ @Nullable
public static String getVersion() {
return isAvailable() ? opusGetVersion() : null;
}
+ /**
+ * Returns whether the given {@link ExoMediaCrypto} type matches the one required for decoding
+ * protected content.
+ */
+ public static boolean matchesExpectedExoMediaCryptoType(
+ @Nullable Class extends ExoMediaCrypto> exoMediaCryptoType) {
+ return Util.areEqual(OpusLibrary.exoMediaCryptoType, exoMediaCryptoType);
+ }
+
public static native String opusGetVersion();
public static native boolean opusIsSecureDecodeSupported();
}
diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/package-info.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/package-info.java
new file mode 100644
index 0000000000..0848937fdc
--- /dev/null
+++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+@NonNullApi
+package com.google.android.exoplayer2.ext.opus;
+
+import com.google.android.exoplayer2.util.NonNullApi;
diff --git a/extensions/opus/src/main/jni/Android.mk b/extensions/opus/src/main/jni/Android.mk
index 9d1e4fe726..0b06d9ecd8 100644
--- a/extensions/opus/src/main/jni/Android.mk
+++ b/extensions/opus/src/main/jni/Android.mk
@@ -21,10 +21,10 @@ include $(CLEAR_VARS)
LOCAL_PATH := $(WORKING_DIR)
include libopus.mk
-# build libopusJNI.so
+# build libopusV2JNI.so
include $(CLEAR_VARS)
LOCAL_PATH := $(WORKING_DIR)
-LOCAL_MODULE := libopusJNI
+LOCAL_MODULE := libopusV2JNI
LOCAL_ARM_MODE := arm
LOCAL_CPP_EXTENSION := .cc
LOCAL_SRC_FILES := opus_jni.cc
diff --git a/extensions/opus/src/test/AndroidManifest.xml b/extensions/opus/src/test/AndroidManifest.xml
new file mode 100644
index 0000000000..d17f889d17
--- /dev/null
+++ b/extensions/opus/src/test/AndroidManifest.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
diff --git a/extensions/opus/src/test/java/com/google/android/exoplayer2/ext/opus/DefaultRenderersFactoryTest.java b/extensions/opus/src/test/java/com/google/android/exoplayer2/ext/opus/DefaultRenderersFactoryTest.java
new file mode 100644
index 0000000000..e57ad84a41
--- /dev/null
+++ b/extensions/opus/src/test/java/com/google/android/exoplayer2/ext/opus/DefaultRenderersFactoryTest.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2019 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.opus;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.testutil.DefaultRenderersFactoryAsserts;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit test for {@link DefaultRenderersFactoryTest} with {@link LibopusAudioRenderer}. */
+@RunWith(AndroidJUnit4.class)
+public final class DefaultRenderersFactoryTest {
+
+ @Test
+ public void createRenderers_instantiatesVpxRenderer() {
+ DefaultRenderersFactoryAsserts.assertExtensionRendererCreated(
+ LibopusAudioRenderer.class, C.TRACK_TYPE_AUDIO);
+ }
+}
diff --git a/extensions/rtmp/README.md b/extensions/rtmp/README.md
index b222bdabd9..a34341692b 100644
--- a/extensions/rtmp/README.md
+++ b/extensions/rtmp/README.md
@@ -3,7 +3,7 @@
The RTMP extension is a [DataSource][] implementation for playing [RTMP][]
streams using [LibRtmp Client for Android][].
-[DataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/upstream/DataSource.html
+[DataSource]: https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/upstream/DataSource.html
[RTMP]: https://en.wikipedia.org/wiki/Real-Time_Messaging_Protocol
[LibRtmp Client for Android]: https://github.com/ant-media/LibRtmp-Client-for-Android
@@ -39,7 +39,7 @@ either instantiated and injected from application code, or obtained from
instances of `DataSource.Factory` that are instantiated and injected from
application code.
-`DefaultDataSource` will automatically use uses the RTMP extension whenever it's
+`DefaultDataSource` will automatically use the RTMP extension whenever it's
available. Hence if your application is using `DefaultDataSource` or
`DefaultDataSourceFactory`, adding support for RTMP streams is as simple as
adding a dependency to the RTMP extension as described above. No changes to your
@@ -53,4 +53,4 @@ doesn't need to handle any other protocols, you can update any `DataSource`s and
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.rtmp.*`
belong to this module.
-[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
+[Javadoc]: https://exoplayer.dev/doc/reference/index.html
diff --git a/extensions/rtmp/build.gradle b/extensions/rtmp/build.gradle
index 2f2c65980a..74ef70fbf0 100644
--- a/extensions/rtmp/build.gradle
+++ b/extensions/rtmp/build.gradle
@@ -16,7 +16,6 @@ apply plugin: 'com.android.library'
android {
compileSdkVersion project.ext.compileSdkVersion
- buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@@ -24,15 +23,19 @@ android {
}
defaultConfig {
- minSdkVersion 15
+ minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
}
+
+ testOptions.unitTests.includeAndroidResources = true
}
dependencies {
implementation project(modulePrefix + 'library-core')
implementation 'net.butterflytv.utils:rtmp-client:3.0.1'
- implementation 'com.android.support:support-annotations:' + supportLibraryVersion
+ implementation 'androidx.annotation:annotation:1.1.0'
+ testImplementation project(modulePrefix + 'testutils')
+ testImplementation 'org.robolectric:robolectric:' + robolectricVersion
}
ext {
diff --git a/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSource.java b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSource.java
index 08c328ce81..587e310d64 100644
--- a/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSource.java
+++ b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSource.java
@@ -15,14 +15,15 @@
*/
package com.google.android.exoplayer2.ext.rtmp;
+import static com.google.android.exoplayer2.util.Util.castNonNull;
+
import android.net.Uri;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.upstream.BaseDataSource;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
-import com.google.android.exoplayer2.upstream.TransferListener;
import java.io.IOException;
import net.butterflytv.rtmp_client.RtmpClient;
import net.butterflytv.rtmp_client.RtmpClient.RtmpIOException;
@@ -34,25 +35,13 @@ public final class RtmpDataSource extends BaseDataSource {
ExoPlayerLibraryInfo.registerModule("goog.exo.rtmp");
}
- private RtmpClient rtmpClient;
- private Uri uri;
+ @Nullable private RtmpClient rtmpClient;
+ @Nullable private Uri uri;
public RtmpDataSource() {
super(/* isNetwork= */ true);
}
- /**
- * @param listener An optional listener.
- * @deprecated Use {@link #RtmpDataSource()} and {@link #addTransferListener(TransferListener)}.
- */
- @Deprecated
- public RtmpDataSource(@Nullable TransferListener listener) {
- this();
- if (listener != null) {
- addTransferListener(listener);
- }
- }
-
@Override
public long open(DataSpec dataSpec) throws RtmpIOException {
transferInitializing(dataSpec);
@@ -66,7 +55,7 @@ public final class RtmpDataSource extends BaseDataSource {
@Override
public int read(byte[] buffer, int offset, int readLength) throws IOException {
- int bytesRead = rtmpClient.read(buffer, offset, readLength);
+ int bytesRead = castNonNull(rtmpClient).read(buffer, offset, readLength);
if (bytesRead == -1) {
return C.RESULT_END_OF_INPUT;
}
@@ -87,6 +76,7 @@ public final class RtmpDataSource extends BaseDataSource {
}
@Override
+ @Nullable
public Uri getUri() {
return uri;
}
diff --git a/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSourceFactory.java b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSourceFactory.java
index d1350276f2..505724e846 100644
--- a/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSourceFactory.java
+++ b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSourceFactory.java
@@ -15,7 +15,7 @@
*/
package com.google.android.exoplayer2.ext.rtmp;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
import com.google.android.exoplayer2.upstream.TransferListener;
@@ -25,7 +25,7 @@ import com.google.android.exoplayer2.upstream.TransferListener;
*/
public final class RtmpDataSourceFactory implements DataSource.Factory {
- private final @Nullable TransferListener listener;
+ @Nullable private final TransferListener listener;
public RtmpDataSourceFactory() {
this(null);
diff --git a/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/package-info.java b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/package-info.java
new file mode 100644
index 0000000000..cb16630bd3
--- /dev/null
+++ b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+@NonNullApi
+package com.google.android.exoplayer2.ext.rtmp;
+
+import com.google.android.exoplayer2.util.NonNullApi;
diff --git a/demos/main/src/main/res/layout/start_download_dialog.xml b/extensions/rtmp/src/test/AndroidManifest.xml
similarity index 77%
rename from demos/main/src/main/res/layout/start_download_dialog.xml
rename to extensions/rtmp/src/test/AndroidManifest.xml
index acb9af5d97..b2e19827d9 100644
--- a/demos/main/src/main/res/layout/start_download_dialog.xml
+++ b/extensions/rtmp/src/test/AndroidManifest.xml
@@ -13,7 +13,7 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
+
+
+
+
diff --git a/extensions/rtmp/src/test/java/com/google/android/exoplayer2/ext/rtmp/DefaultDataSourceTest.java b/extensions/rtmp/src/test/java/com/google/android/exoplayer2/ext/rtmp/DefaultDataSourceTest.java
new file mode 100644
index 0000000000..469e66a884
--- /dev/null
+++ b/extensions/rtmp/src/test/java/com/google/android/exoplayer2/ext/rtmp/DefaultDataSourceTest.java
@@ -0,0 +1,47 @@
+/*
+ * 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.rtmp;
+
+import android.net.Uri;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.upstream.DefaultDataSource;
+import java.io.IOException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit test for {@link DefaultDataSource} with RTMP URIs. */
+@RunWith(AndroidJUnit4.class)
+public final class DefaultDataSourceTest {
+
+ @Test
+ public void openRtmpDataSpec_instantiatesRtmpDataSourceViaReflection() throws IOException {
+ DefaultDataSource dataSource =
+ new DefaultDataSource(
+ ApplicationProvider.getApplicationContext(),
+ "userAgent",
+ /* allowCrossProtocolRedirects= */ false);
+ DataSpec dataSpec = new DataSpec(Uri.parse("rtmp://test.com/stream"));
+ try {
+ dataSource.open(dataSpec);
+ } catch (UnsatisfiedLinkError e) {
+ // RtmpDataSource was successfully instantiated (test run using Gradle).
+ } catch (UnsupportedOperationException e) {
+ // RtmpDataSource was successfully instantiated (test run using Blaze).
+ }
+ }
+}
diff --git a/extensions/vp9/README.md b/extensions/vp9/README.md
index 9601829c91..be75eae359 100644
--- a/extensions/vp9/README.md
+++ b/extensions/vp9/README.md
@@ -29,36 +29,33 @@ VP9_EXT_PATH="${EXOPLAYER_ROOT}/extensions/vp9/src/main"
```
* Download the [Android NDK][] and set its location in an environment variable.
+ The build configuration has been tested with Android NDK r19c.
```
NDK_PATH=""
```
-* Fetch libvpx and libyuv:
+* Fetch libvpx:
```
cd "${VP9_EXT_PATH}/jni" && \
-git clone https://chromium.googlesource.com/webm/libvpx libvpx && \
-git clone https://chromium.googlesource.com/libyuv/libyuv libyuv
+git clone https://chromium.googlesource.com/webm/libvpx libvpx
```
-* Checkout the appropriate branches of libvpx and libyuv (the scripts and
- makefiles bundled in this repo are known to work only at these versions of the
- libraries - we will update this periodically as newer versions of
- libvpx/libyuv are released):
+* Checkout the appropriate branch of libvpx (the scripts and makefiles bundled
+ in this repo are known to work only at specific versions of the library - we
+ will update this periodically as newer versions of libvpx are released):
```
cd "${VP9_EXT_PATH}/jni/libvpx" && \
-git checkout tags/v1.6.1 -b v1.6.1 && \
-cd "${VP9_EXT_PATH}/jni/libyuv" && \
-git checkout 996a2bbd
+git checkout tags/v1.8.0 -b v1.8.0
```
* Run a script that generates necessary configuration files for libvpx:
```
cd ${VP9_EXT_PATH}/jni && \
-./generate_libvpx_android_configs.sh "${NDK_PATH}"
+./generate_libvpx_android_configs.sh
```
* Build the JNI native libraries from the command line:
@@ -70,7 +67,6 @@ ${NDK_PATH}/ndk-build APP_ABI=all -j4
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
-[#3520]: https://github.com/google/ExoPlayer/issues/3520
## Notes ##
@@ -78,10 +74,10 @@ ${NDK_PATH}/ndk-build APP_ABI=all -j4
* Android config scripts should be re-generated by running
`generate_libvpx_android_configs.sh`
* Clean and re-build the project.
-* If you want to use your own version of libvpx or libyuv, place it in
- `${VP9_EXT_PATH}/jni/libvpx` or `${VP9_EXT_PATH}/jni/libyuv` respectively. But
- please note that `generate_libvpx_android_configs.sh` and the makefiles need
- to be modified to work with arbitrary versions of libvpx and libyuv.
+* If you want to use your own version of libvpx, place it in
+ `${VP9_EXT_PATH}/jni/libvpx`. Please note that
+ `generate_libvpx_android_configs.sh` and the makefiles may need to be modified
+ to work with arbitrary versions of libvpx.
## Using the extension ##
@@ -123,4 +119,4 @@ type `LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER` with the
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.vp9.*`
belong to this module.
-[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
+[Javadoc]: https://exoplayer.dev/doc/reference/index.html
diff --git a/extensions/vp9/build.gradle b/extensions/vp9/build.gradle
index 96c58d7a57..3b8271869b 100644
--- a/extensions/vp9/build.gradle
+++ b/extensions/vp9/build.gradle
@@ -16,7 +16,6 @@ apply plugin: 'com.android.library'
android {
compileSdkVersion project.ext.compileSdkVersion
- buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@@ -34,12 +33,17 @@ android {
jniLibs.srcDir 'src/main/libs'
jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio.
}
+
+ testOptions.unitTests.includeAndroidResources = true
}
dependencies {
implementation project(modulePrefix + 'library-core')
- implementation 'com.android.support:support-annotations:' + supportLibraryVersion
- androidTestImplementation 'androidx.test:runner:' + testRunnerVersion
+ implementation 'androidx.annotation:annotation:1.1.0'
+ testImplementation project(modulePrefix + 'testutils')
+ testImplementation 'org.robolectric:robolectric:' + robolectricVersion
+ androidTestImplementation 'androidx.test:runner:' + androidXTestVersion
+ androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion
androidTestImplementation 'com.google.truth:truth:' + truthVersion
}
diff --git a/extensions/vp9/src/androidTest/AndroidManifest.xml b/extensions/vp9/src/androidTest/AndroidManifest.xml
index 214427c4f0..6ca2e7164a 100644
--- a/extensions/vp9/src/androidTest/AndroidManifest.xml
+++ b/extensions/vp9/src/androidTest/AndroidManifest.xml
@@ -18,6 +18,9 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.google.android.exoplayer2.ext.vp9.test">
+
+
+
diff --git a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java
index d06e2934fb..9be1d9c0e5 100644
--- a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java
+++ b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java
@@ -15,25 +15,25 @@
*/
package com.google.android.exoplayer2.ext.vp9;
-import static androidx.test.InstrumentationRegistry.getContext;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import android.content.Context;
import android.net.Uri;
import android.os.Looper;
-import android.util.Log;
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
-import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
+import com.google.android.exoplayer2.util.Log;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -89,7 +89,7 @@ public class VpxPlaybackTest {
private void playUri(String uri) throws Exception {
TestPlaybackRunnable testPlaybackRunnable =
- new TestPlaybackRunnable(Uri.parse(uri), getContext());
+ new TestPlaybackRunnable(Uri.parse(uri), ApplicationProvider.getApplicationContext());
Thread thread = new Thread(testPlaybackRunnable);
thread.start();
thread.join();
@@ -114,14 +114,14 @@ public class VpxPlaybackTest {
@Override
public void run() {
Looper.prepare();
- LibvpxVideoRenderer videoRenderer = new LibvpxVideoRenderer(true, 0);
- DefaultTrackSelector trackSelector = new DefaultTrackSelector();
- player = ExoPlayerFactory.newInstance(new Renderer[] {videoRenderer}, trackSelector);
+ LibvpxVideoRenderer videoRenderer = new LibvpxVideoRenderer(0);
+ DefaultTrackSelector trackSelector = new DefaultTrackSelector(context);
+ player = ExoPlayerFactory.newInstance(context, new Renderer[] {videoRenderer}, trackSelector);
player.addListener(this);
MediaSource mediaSource =
- new ExtractorMediaSource.Factory(
- new DefaultDataSourceFactory(context, "ExoPlayerExtVp9Test"))
- .setExtractorsFactory(MatroskaExtractor.FACTORY)
+ new ProgressiveMediaSource.Factory(
+ new DefaultDataSourceFactory(context, "ExoPlayerExtVp9Test"),
+ MatroskaExtractor.FACTORY)
.createMediaSource(uri);
player
.createMessage(videoRenderer)
@@ -139,7 +139,7 @@ public class VpxPlaybackTest {
}
@Override
- public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
+ public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
if (playbackState == Player.STATE_ENDED
|| (playbackState == Player.STATE_IDLE && playbackException != null)) {
player.release();
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 f0986d08be..b000ea1b6b 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
@@ -15,14 +15,14 @@
*/
package com.google.android.exoplayer2.ext.vp9;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
+import static java.lang.Runtime.getRuntime;
+
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
-import android.support.annotation.CallSuper;
-import android.support.annotation.IntDef;
-import android.support.annotation.Nullable;
+import androidx.annotation.CallSuper;
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
import android.view.Surface;
import com.google.android.exoplayer2.BaseRenderer;
import com.google.android.exoplayer2.C;
@@ -42,9 +42,11 @@ import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.TimedValueQueue;
import com.google.android.exoplayer2.util.TraceUtil;
import com.google.android.exoplayer2.util.Util;
+import com.google.android.exoplayer2.video.VideoDecoderInputBuffer;
import com.google.android.exoplayer2.video.VideoFrameMetadataListener;
import com.google.android.exoplayer2.video.VideoRendererEventListener;
import com.google.android.exoplayer2.video.VideoRendererEventListener.EventDispatcher;
+import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -64,9 +66,13 @@ import java.lang.annotation.RetentionPolicy;
*/
public class LibvpxVideoRenderer extends BaseRenderer {
+ @Documented
@Retention(RetentionPolicy.SOURCE)
- @IntDef({REINITIALIZATION_STATE_NONE, REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM,
- REINITIALIZATION_STATE_WAIT_END_OF_STREAM})
+ @IntDef({
+ REINITIALIZATION_STATE_NONE,
+ REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM,
+ REINITIALIZATION_STATE_WAIT_END_OF_STREAM
+ })
private @interface ReinitializationState {}
/**
* The decoder does not need to be re-initialized.
@@ -92,19 +98,17 @@ public class LibvpxVideoRenderer extends BaseRenderer {
*/
public static final int MSG_SET_OUTPUT_BUFFER_RENDERER = C.MSG_CUSTOM_BASE;
- /**
- * The number of input buffers.
- */
- private static final int NUM_INPUT_BUFFERS = 8;
+ /** The number of input buffers. */
+ private final int numInputBuffers;
/**
* The number of output buffers. The renderer may limit the minimum possible value due to
* requiring multiple output buffers to be dequeued at a time for it to make progress.
*/
- private static final int NUM_OUTPUT_BUFFERS = 8;
+ private final int numOutputBuffers;
/** The default input buffer size. */
private static final int DEFAULT_INPUT_BUFFER_SIZE = 768 * 1024; // Value based on cs/SoftVpx.cpp.
- private final boolean scaleToFit;
+ private final boolean enableRowMultiThreadMode;
private final boolean disableLoopFilter;
private final long allowedJoiningTimeMs;
private final int maxDroppedFramesToNotify;
@@ -114,27 +118,26 @@ public class LibvpxVideoRenderer extends BaseRenderer {
private final TimedValueQueue formatQueue;
private final DecoderInputBuffer flagsOnlyBuffer;
private final DrmSessionManager drmSessionManager;
- private final boolean useSurfaceYuvOutput;
+ private final int threads;
private Format format;
private Format pendingFormat;
private Format outputFormat;
private VpxDecoder decoder;
- private VpxInputBuffer inputBuffer;
+ private VideoDecoderInputBuffer inputBuffer;
private VpxOutputBuffer outputBuffer;
- private DrmSession drmSession;
- private DrmSession pendingDrmSession;
+ @Nullable private DrmSession decoderDrmSession;
+ @Nullable private DrmSession sourceDrmSession;
private @ReinitializationState int decoderReinitializationState;
private boolean decoderReceivedBuffers;
- private Bitmap bitmap;
private boolean renderedFirstFrame;
private long initialPositionUs;
private long joiningDeadlineMs;
private Surface surface;
private VpxOutputBufferRenderer outputBufferRenderer;
- private int outputMode;
+ @C.VideoOutputMode private int outputMode;
private boolean waitingForKeys;
private boolean inputStreamEnded;
@@ -153,16 +156,14 @@ public class LibvpxVideoRenderer extends BaseRenderer {
protected DecoderCounters decoderCounters;
/**
- * @param scaleToFit Whether video frames should be scaled to fit when rendering.
* @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
* can attempt to seamlessly join an ongoing playback.
*/
- public LibvpxVideoRenderer(boolean scaleToFit, long allowedJoiningTimeMs) {
- this(scaleToFit, allowedJoiningTimeMs, null, null, 0);
+ public LibvpxVideoRenderer(long allowedJoiningTimeMs) {
+ this(allowedJoiningTimeMs, null, null, 0);
}
/**
- * @param scaleToFit Whether video frames should be scaled to fit when rendering.
* @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
* can attempt to seamlessly join an ongoing playback.
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
@@ -171,23 +172,22 @@ public class LibvpxVideoRenderer extends BaseRenderer {
* @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between
* invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}.
*/
- public LibvpxVideoRenderer(boolean scaleToFit, long allowedJoiningTimeMs,
- Handler eventHandler, VideoRendererEventListener eventListener,
+ public LibvpxVideoRenderer(
+ long allowedJoiningTimeMs,
+ @Nullable Handler eventHandler,
+ @Nullable VideoRendererEventListener eventListener,
int maxDroppedFramesToNotify) {
this(
- scaleToFit,
allowedJoiningTimeMs,
eventHandler,
eventListener,
maxDroppedFramesToNotify,
/* drmSessionManager= */ null,
/* playClearSamplesWithoutKeys= */ false,
- /* disableLoopFilter= */ false,
- /* useSurfaceYuvOutput= */ false);
+ /* disableLoopFilter= */ false);
}
/**
- * @param scaleToFit Whether video frames should be scaled to fit when rendering.
* @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
* can attempt to seamlessly join an ongoing playback.
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
@@ -203,33 +203,79 @@ public class LibvpxVideoRenderer extends BaseRenderer {
* permitted to play clear regions of encrypted media files before {@code drmSessionManager}
* has obtained the keys necessary to decrypt encrypted regions of the media.
* @param disableLoopFilter Disable the libvpx in-loop smoothing filter.
- * @param useSurfaceYuvOutput Directly output YUV to the Surface via ANativeWindow.
*/
public LibvpxVideoRenderer(
- boolean scaleToFit,
long allowedJoiningTimeMs,
- Handler eventHandler,
- VideoRendererEventListener eventListener,
+ @Nullable Handler eventHandler,
+ @Nullable VideoRendererEventListener eventListener,
int maxDroppedFramesToNotify,
- DrmSessionManager drmSessionManager,
+ @Nullable DrmSessionManager drmSessionManager,
+ boolean playClearSamplesWithoutKeys,
+ boolean disableLoopFilter) {
+ this(
+ allowedJoiningTimeMs,
+ eventHandler,
+ eventListener,
+ maxDroppedFramesToNotify,
+ drmSessionManager,
+ playClearSamplesWithoutKeys,
+ disableLoopFilter,
+ /* enableRowMultiThreadMode= */ false,
+ getRuntime().availableProcessors(),
+ /* numInputBuffers= */ 4,
+ /* numOutputBuffers= */ 4);
+ }
+
+ /**
+ * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
+ * can attempt to seamlessly join an ongoing playback.
+ * @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 maxDroppedFramesToNotify The maximum number of frames that can be dropped between
+ * invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}.
+ * @param drmSessionManager For use with encrypted media. May be null if support for encrypted
+ * media is not required.
+ * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.
+ * For example a media file may start with a short clear region so as to allow playback to
+ * begin in parallel with key acquisition. This parameter specifies whether the renderer is
+ * permitted to play clear regions of encrypted media files before {@code drmSessionManager}
+ * has obtained the keys necessary to decrypt encrypted regions of the media.
+ * @param disableLoopFilter Disable the libvpx in-loop smoothing filter.
+ * @param enableRowMultiThreadMode Whether row multi threading decoding is enabled.
+ * @param threads Number of threads libvpx will use to decode.
+ * @param numInputBuffers Number of input buffers.
+ * @param numOutputBuffers Number of output buffers.
+ */
+ public LibvpxVideoRenderer(
+ long allowedJoiningTimeMs,
+ @Nullable Handler eventHandler,
+ @Nullable VideoRendererEventListener eventListener,
+ int maxDroppedFramesToNotify,
+ @Nullable DrmSessionManager drmSessionManager,
boolean playClearSamplesWithoutKeys,
boolean disableLoopFilter,
- boolean useSurfaceYuvOutput) {
+ boolean enableRowMultiThreadMode,
+ int threads,
+ int numInputBuffers,
+ int numOutputBuffers) {
super(C.TRACK_TYPE_VIDEO);
- this.scaleToFit = scaleToFit;
this.disableLoopFilter = disableLoopFilter;
this.allowedJoiningTimeMs = allowedJoiningTimeMs;
this.maxDroppedFramesToNotify = maxDroppedFramesToNotify;
this.drmSessionManager = drmSessionManager;
this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
- this.useSurfaceYuvOutput = useSurfaceYuvOutput;
+ this.enableRowMultiThreadMode = enableRowMultiThreadMode;
+ this.threads = threads;
+ this.numInputBuffers = numInputBuffers;
+ this.numOutputBuffers = numOutputBuffers;
joiningDeadlineMs = C.TIME_UNSET;
clearReportedVideoSize();
formatHolder = new FormatHolder();
formatQueue = new TimedValueQueue<>();
flagsOnlyBuffer = DecoderInputBuffer.newFlagsOnlyInstance();
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
- outputMode = VpxDecoder.OUTPUT_MODE_NONE;
+ outputMode = C.VIDEO_OUTPUT_MODE_NONE;
decoderReinitializationState = REINITIALIZATION_STATE_NONE;
}
@@ -239,7 +285,13 @@ public class LibvpxVideoRenderer extends BaseRenderer {
public int supportsFormat(Format format) {
if (!VpxLibrary.isAvailable() || !MimeTypes.VIDEO_VP9.equalsIgnoreCase(format.sampleMimeType)) {
return FORMAT_UNSUPPORTED_TYPE;
- } else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {
+ }
+ boolean drmIsSupported =
+ format.drmInitData == null
+ || VpxLibrary.matchesExpectedExoMediaCryptoType(format.exoMediaCryptoType)
+ || (format.exoMediaCryptoType == null
+ && supportsFormatDrm(drmSessionManager, format.drmInitData));
+ if (!drmIsSupported) {
return FORMAT_UNSUPPORTED_DRM;
}
return FORMAT_HANDLED | ADAPTIVE_SEAMLESS;
@@ -256,7 +308,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
flagsOnlyBuffer.clear();
int result = readSource(formatHolder, flagsOnlyBuffer, true);
if (result == C.RESULT_FORMAT_READ) {
- onInputFormatChanged(formatHolder.format);
+ onInputFormatChanged(formatHolder);
} else if (result == C.RESULT_BUFFER_READ) {
// End of stream read having not read a format.
Assertions.checkState(flagsOnlyBuffer.isEndOfStream());
@@ -297,8 +349,9 @@ public class LibvpxVideoRenderer extends BaseRenderer {
if (waitingForKeys) {
return false;
}
- if (format != null && (isSourceReady() || outputBuffer != null)
- && (renderedFirstFrame || outputMode == VpxDecoder.OUTPUT_MODE_NONE)) {
+ if (format != null
+ && (isSourceReady() || outputBuffer != null)
+ && (renderedFirstFrame || outputMode == C.VIDEO_OUTPUT_MODE_NONE)) {
// Ready. If we were joining then we've now joined, so clear the joining deadline.
joiningDeadlineMs = C.TIME_UNSET;
return true;
@@ -359,24 +412,10 @@ public class LibvpxVideoRenderer extends BaseRenderer {
clearReportedVideoSize();
clearRenderedFirstFrame();
try {
+ setSourceDrmSession(null);
releaseDecoder();
} finally {
- try {
- if (drmSession != null) {
- drmSessionManager.releaseSession(drmSession);
- }
- } finally {
- try {
- if (pendingDrmSession != null && pendingDrmSession != drmSession) {
- drmSessionManager.releaseSession(pendingDrmSession);
- }
- } finally {
- drmSession = null;
- pendingDrmSession = null;
- decoderCounters.ensureUpdated();
- eventDispatcher.disabled(decoderCounters);
- }
- }
+ eventDispatcher.disabled(decoderCounters);
}
}
@@ -428,50 +467,66 @@ public class LibvpxVideoRenderer extends BaseRenderer {
/** Releases the decoder. */
@CallSuper
protected void releaseDecoder() {
- if (decoder == null) {
- return;
- }
-
inputBuffer = null;
outputBuffer = null;
- decoder.release();
- decoder = null;
- decoderCounters.decoderReleaseCount++;
decoderReinitializationState = REINITIALIZATION_STATE_NONE;
decoderReceivedBuffers = false;
buffersInCodecCount = 0;
+ if (decoder != null) {
+ decoder.release();
+ decoder = null;
+ decoderCounters.decoderReleaseCount++;
+ }
+ setDecoderDrmSession(null);
+ }
+
+ private void setSourceDrmSession(@Nullable DrmSession session) {
+ DrmSession.replaceSessionReferences(sourceDrmSession, session);
+ sourceDrmSession = session;
+ }
+
+ private void setDecoderDrmSession(@Nullable DrmSession session) {
+ DrmSession.replaceSessionReferences(decoderDrmSession, session);
+ decoderDrmSession = session;
}
/**
* Called when a new format is read from the upstream source.
*
- * @param newFormat The new format.
+ * @param formatHolder A {@link FormatHolder} that holds the new {@link Format}.
* @throws ExoPlaybackException If an error occurs (re-)initializing the decoder.
*/
@CallSuper
- protected void onInputFormatChanged(Format newFormat) throws ExoPlaybackException {
+ @SuppressWarnings("unchecked")
+ protected void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException {
Format oldFormat = format;
- format = newFormat;
- pendingFormat = newFormat;
+ format = formatHolder.format;
+ pendingFormat = format;
boolean drmInitDataChanged = !Util.areEqual(format.drmInitData, oldFormat == null ? null
: oldFormat.drmInitData);
if (drmInitDataChanged) {
if (format.drmInitData != null) {
- if (drmSessionManager == null) {
- throw ExoPlaybackException.createForRenderer(
- new IllegalStateException("Media requires a DrmSessionManager"), getIndex());
- }
- pendingDrmSession = drmSessionManager.acquireSession(Looper.myLooper(), format.drmInitData);
- if (pendingDrmSession == drmSession) {
- drmSessionManager.releaseSession(pendingDrmSession);
+ if (formatHolder.includesDrmSession) {
+ setSourceDrmSession((DrmSession) formatHolder.drmSession);
+ } else {
+ if (drmSessionManager == null) {
+ throw ExoPlaybackException.createForRenderer(
+ new IllegalStateException("Media requires a DrmSessionManager"), getIndex());
+ }
+ DrmSession session =
+ drmSessionManager.acquireSession(Looper.myLooper(), format.drmInitData);
+ if (sourceDrmSession != null) {
+ sourceDrmSession.releaseReference();
+ }
+ sourceDrmSession = session;
}
} else {
- pendingDrmSession = null;
+ setSourceDrmSession(null);
}
}
- if (pendingDrmSession != drmSession) {
+ if (sourceDrmSession != decoderDrmSession) {
if (decoderReceivedBuffers) {
// Signal end of stream and wait for any final output buffers before re-initialization.
decoderReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM;
@@ -492,7 +547,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
*
* @param buffer The buffer that will be queued.
*/
- protected void onQueueInputBuffer(VpxInputBuffer buffer) {
+ protected void onQueueInputBuffer(VideoDecoderInputBuffer buffer) {
// Do nothing.
}
@@ -574,18 +629,14 @@ public class LibvpxVideoRenderer extends BaseRenderer {
*/
protected void renderOutputBuffer(VpxOutputBuffer outputBuffer) throws VpxDecoderException {
int bufferMode = outputBuffer.mode;
- boolean renderRgb = bufferMode == VpxDecoder.OUTPUT_MODE_RGB && surface != null;
- boolean renderSurface = bufferMode == VpxDecoder.OUTPUT_MODE_SURFACE_YUV && surface != null;
- boolean renderYuv = bufferMode == VpxDecoder.OUTPUT_MODE_YUV && outputBufferRenderer != null;
+ boolean renderSurface = bufferMode == C.VIDEO_OUTPUT_MODE_SURFACE_YUV && surface != null;
+ boolean renderYuv = bufferMode == C.VIDEO_OUTPUT_MODE_YUV && outputBufferRenderer != null;
lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000;
- if (!renderRgb && !renderYuv && !renderSurface) {
+ if (!renderYuv && !renderSurface) {
dropOutputBuffer(outputBuffer);
} else {
maybeNotifyVideoSizeChanged(outputBuffer.width, outputBuffer.height);
- if (renderRgb) {
- renderRgbFrame(outputBuffer, scaleToFit);
- outputBuffer.release();
- } else if (renderYuv) {
+ if (renderYuv) {
outputBufferRenderer.setOutputBuffer(outputBuffer);
// The renderer will release the buffer.
} else { // renderSurface
@@ -632,7 +683,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
consecutiveDroppedFrameCount += droppedBufferCount;
decoderCounters.maxConsecutiveDroppedBufferCount =
Math.max(consecutiveDroppedFrameCount, decoderCounters.maxConsecutiveDroppedBufferCount);
- if (droppedFrames >= maxDroppedFramesToNotify) {
+ if (maxDroppedFramesToNotify > 0 && droppedFrames >= maxDroppedFramesToNotify) {
maybeNotifyDroppedFrames();
}
}
@@ -640,7 +691,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
// PlayerMessage.Target implementation.
@Override
- public void handleMessage(int messageType, Object message) throws ExoPlaybackException {
+ public void handleMessage(int messageType, @Nullable Object message) throws ExoPlaybackException {
if (messageType == C.MSG_SET_SURFACE) {
setOutput((Surface) message, null);
} else if (messageType == MSG_SET_OUTPUT_BUFFER_RENDERER) {
@@ -663,13 +714,12 @@ public class LibvpxVideoRenderer extends BaseRenderer {
this.surface = surface;
this.outputBufferRenderer = outputBufferRenderer;
if (surface != null) {
- outputMode =
- useSurfaceYuvOutput ? VpxDecoder.OUTPUT_MODE_SURFACE_YUV : VpxDecoder.OUTPUT_MODE_RGB;
+ outputMode = C.VIDEO_OUTPUT_MODE_SURFACE_YUV;
} else {
outputMode =
- outputBufferRenderer != null ? VpxDecoder.OUTPUT_MODE_YUV : VpxDecoder.OUTPUT_MODE_NONE;
+ outputBufferRenderer != null ? C.VIDEO_OUTPUT_MODE_YUV : C.VIDEO_OUTPUT_MODE_NONE;
}
- if (outputMode != VpxDecoder.OUTPUT_MODE_NONE) {
+ if (outputMode != C.VIDEO_OUTPUT_MODE_NONE) {
if (decoder != null) {
decoder.setOutputMode(outputMode);
}
@@ -686,7 +736,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
clearReportedVideoSize();
clearRenderedFirstFrame();
}
- } else if (outputMode != VpxDecoder.OUTPUT_MODE_NONE) {
+ } else if (outputMode != C.VIDEO_OUTPUT_MODE_NONE) {
// The output is unchanged and non-null. If we know the video size and/or have already
// rendered to the output, report these again immediately.
maybeRenotifyVideoSizeChanged();
@@ -699,12 +749,13 @@ public class LibvpxVideoRenderer extends BaseRenderer {
return;
}
- drmSession = pendingDrmSession;
+ setDecoderDrmSession(sourceDrmSession);
+
ExoMediaCrypto mediaCrypto = null;
- if (drmSession != null) {
- mediaCrypto = drmSession.getMediaCrypto();
+ if (decoderDrmSession != null) {
+ mediaCrypto = decoderDrmSession.getMediaCrypto();
if (mediaCrypto == null) {
- DrmSessionException drmError = drmSession.getError();
+ DrmSessionException drmError = decoderDrmSession.getError();
if (drmError != null) {
// Continue for now. We may be able to avoid failure if the session recovers, or if a new
// input format causes the session to be replaced before it's used.
@@ -722,12 +773,13 @@ public class LibvpxVideoRenderer extends BaseRenderer {
format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE;
decoder =
new VpxDecoder(
- NUM_INPUT_BUFFERS,
- NUM_OUTPUT_BUFFERS,
+ numInputBuffers,
+ numOutputBuffers,
initialInputBufferSize,
mediaCrypto,
disableLoopFilter,
- useSurfaceYuvOutput);
+ enableRowMultiThreadMode,
+ threads);
decoder.setOutputMode(outputMode);
TraceUtil.endSection();
long decoderInitializedTimestamp = SystemClock.elapsedRealtime();
@@ -776,7 +828,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
return false;
}
if (result == C.RESULT_FORMAT_READ) {
- onInputFormatChanged(formatHolder.format);
+ onInputFormatChanged(formatHolder);
return true;
}
if (inputBuffer.isEndOfStream()) {
@@ -795,7 +847,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
pendingFormat = null;
}
inputBuffer.flip();
- inputBuffer.colorInfo = formatHolder.format.colorInfo;
+ inputBuffer.colorInfo = format.colorInfo;
onQueueInputBuffer(inputBuffer);
decoder.queueInputBuffer(inputBuffer);
buffersInCodecCount++;
@@ -864,7 +916,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
}
long earlyUs = outputBuffer.timeUs - positionUs;
- if (outputMode == VpxDecoder.OUTPUT_MODE_NONE) {
+ if (outputMode == C.VIDEO_OUTPUT_MODE_NONE) {
// Skip frames in sync with playback, so we'll be at the right frame if the mode changes.
if (isBufferLate(earlyUs)) {
skipOutputBuffer(outputBuffer);
@@ -917,33 +969,16 @@ public class LibvpxVideoRenderer extends BaseRenderer {
}
private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
- if (drmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
+ if (decoderDrmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
return false;
}
- @DrmSession.State int drmSessionState = drmSession.getState();
+ @DrmSession.State int drmSessionState = decoderDrmSession.getState();
if (drmSessionState == DrmSession.STATE_ERROR) {
- throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
+ throw ExoPlaybackException.createForRenderer(decoderDrmSession.getError(), getIndex());
}
return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS;
}
- private void renderRgbFrame(VpxOutputBuffer outputBuffer, boolean scale) {
- if (bitmap == null
- || bitmap.getWidth() != outputBuffer.width
- || bitmap.getHeight() != outputBuffer.height) {
- bitmap = Bitmap.createBitmap(outputBuffer.width, outputBuffer.height, Bitmap.Config.RGB_565);
- }
- bitmap.copyPixelsFromBuffer(outputBuffer.data);
- Canvas canvas = surface.lockCanvas(null);
- if (scale) {
- canvas.scale(
- ((float) canvas.getWidth()) / outputBuffer.width,
- ((float) canvas.getHeight()) / outputBuffer.height);
- }
- canvas.drawBitmap(bitmap, 0, 0, null);
- surface.unlockCanvasAndPost(canvas);
- }
-
private void setJoiningDeadlineMs() {
joiningDeadlineMs = allowedJoiningTimeMs > 0
? (SystemClock.elapsedRealtime() + allowedJoiningTimeMs) : C.TIME_UNSET;
diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java
index 51ef8e9bcf..0efd4bd0ea 100644
--- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java
+++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java
@@ -15,33 +15,28 @@
*/
package com.google.android.exoplayer2.ext.vp9;
+import androidx.annotation.Nullable;
import android.view.Surface;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.decoder.CryptoInfo;
import com.google.android.exoplayer2.decoder.SimpleDecoder;
import com.google.android.exoplayer2.drm.DecryptionException;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
+import com.google.android.exoplayer2.video.VideoDecoderInputBuffer;
import java.nio.ByteBuffer;
-/**
- * Vpx decoder.
- */
-/* package */ final class VpxDecoder extends
- SimpleDecoder {
-
- public static final int OUTPUT_MODE_NONE = -1;
- public static final int OUTPUT_MODE_YUV = 0;
- public static final int OUTPUT_MODE_RGB = 1;
- public static final int OUTPUT_MODE_SURFACE_YUV = 2;
+/** Vpx decoder. */
+/* package */ final class VpxDecoder
+ extends SimpleDecoder {
private static final int NO_ERROR = 0;
private static final int DECODE_ERROR = 1;
private static final int DRM_ERROR = 2;
- private final ExoMediaCrypto exoMediaCrypto;
+ @Nullable private final ExoMediaCrypto exoMediaCrypto;
private final long vpxDecContext;
- private volatile int outputMode;
+ @C.VideoOutputMode private volatile int outputMode;
/**
* Creates a VP9 decoder.
@@ -52,18 +47,20 @@ import java.nio.ByteBuffer;
* @param exoMediaCrypto The {@link ExoMediaCrypto} object required for decoding encrypted
* content. Maybe null and can be ignored if decoder does not handle encrypted content.
* @param disableLoopFilter Disable the libvpx in-loop smoothing filter.
- * @param enableSurfaceYuvOutputMode Whether OUTPUT_MODE_SURFACE_YUV is allowed.
+ * @param enableRowMultiThreadMode Whether row multi threading decoding is enabled.
+ * @param threads Number of threads libvpx will use to decode.
* @throws VpxDecoderException Thrown if an exception occurs when initializing the decoder.
*/
public VpxDecoder(
int numInputBuffers,
int numOutputBuffers,
int initialInputBufferSize,
- ExoMediaCrypto exoMediaCrypto,
+ @Nullable ExoMediaCrypto exoMediaCrypto,
boolean disableLoopFilter,
- boolean enableSurfaceYuvOutputMode)
+ boolean enableRowMultiThreadMode,
+ int threads)
throws VpxDecoderException {
- super(new VpxInputBuffer[numInputBuffers], new VpxOutputBuffer[numOutputBuffers]);
+ super(new VideoDecoderInputBuffer[numInputBuffers], new VpxOutputBuffer[numOutputBuffers]);
if (!VpxLibrary.isAvailable()) {
throw new VpxDecoderException("Failed to load decoder native libraries.");
}
@@ -71,7 +68,7 @@ import java.nio.ByteBuffer;
if (exoMediaCrypto != null && !VpxLibrary.vpxIsSecureDecodeSupported()) {
throw new VpxDecoderException("Vpx decoder does not support secure decode.");
}
- vpxDecContext = vpxInit(disableLoopFilter, enableSurfaceYuvOutputMode);
+ vpxDecContext = vpxInit(disableLoopFilter, enableRowMultiThreadMode, threads);
if (vpxDecContext == 0) {
throw new VpxDecoderException("Failed to initialize decoder");
}
@@ -86,16 +83,15 @@ import java.nio.ByteBuffer;
/**
* Sets the output mode for frames rendered by the decoder.
*
- * @param outputMode The output mode. One of {@link #OUTPUT_MODE_NONE}, {@link #OUTPUT_MODE_RGB}
- * and {@link #OUTPUT_MODE_YUV}.
+ * @param outputMode The output mode.
*/
- public void setOutputMode(int outputMode) {
+ public void setOutputMode(@C.VideoOutputMode int outputMode) {
this.outputMode = outputMode;
}
@Override
- protected VpxInputBuffer createInputBuffer() {
- return new VpxInputBuffer();
+ protected VideoDecoderInputBuffer createInputBuffer() {
+ return new VideoDecoderInputBuffer();
}
@Override
@@ -107,7 +103,7 @@ import java.nio.ByteBuffer;
protected void releaseOutputBuffer(VpxOutputBuffer buffer) {
// Decode only frames do not acquire a reference on the internal decoder buffer and thus do not
// require a call to vpxReleaseFrame.
- if (outputMode == OUTPUT_MODE_SURFACE_YUV && !buffer.isDecodeOnly()) {
+ if (outputMode == C.VIDEO_OUTPUT_MODE_SURFACE_YUV && !buffer.isDecodeOnly()) {
vpxReleaseFrame(vpxDecContext, buffer);
}
super.releaseOutputBuffer(buffer);
@@ -119,8 +115,9 @@ import java.nio.ByteBuffer;
}
@Override
- protected VpxDecoderException decode(VpxInputBuffer inputBuffer, VpxOutputBuffer outputBuffer,
- boolean reset) {
+ @Nullable
+ protected VpxDecoderException decode(
+ VideoDecoderInputBuffer inputBuffer, VpxOutputBuffer outputBuffer, boolean reset) {
ByteBuffer inputData = inputBuffer.data;
int inputSize = inputData.limit();
CryptoInfo cryptoInfo = inputBuffer.cryptoInfo;
@@ -168,13 +165,24 @@ import java.nio.ByteBuffer;
}
}
- private native long vpxInit(boolean disableLoopFilter, boolean enableSurfaceYuvOutputMode);
+ private native long vpxInit(
+ boolean disableLoopFilter, boolean enableRowMultiThreadMode, int threads);
private native long vpxClose(long context);
private native long vpxDecode(long context, ByteBuffer encoded, int length);
- private native long vpxSecureDecode(long context, ByteBuffer encoded, int length,
- ExoMediaCrypto mediaCrypto, int inputMode, byte[] key, byte[] iv,
- int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData);
+
+ private native long vpxSecureDecode(
+ long context,
+ ByteBuffer encoded,
+ int length,
+ @Nullable ExoMediaCrypto mediaCrypto,
+ int inputMode,
+ byte[] key,
+ byte[] iv,
+ int numSubSamples,
+ int[] numBytesOfClearData,
+ int[] numBytesOfEncryptedData);
+
private native int vpxGetFrame(long context, VpxOutputBuffer outputBuffer);
/**
diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoderException.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoderException.java
index 8de14629d3..b2da9a7ff8 100644
--- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoderException.java
+++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoderException.java
@@ -15,8 +15,10 @@
*/
package com.google.android.exoplayer2.ext.vp9;
+import com.google.android.exoplayer2.video.VideoDecoderException;
+
/** Thrown when a libvpx decoder error occurs. */
-public final class VpxDecoderException extends Exception {
+public final class VpxDecoderException extends VideoDecoderException {
/* package */ VpxDecoderException(String message) {
super(message);
diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java
index 854576b4b2..e620332fc8 100644
--- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java
+++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java
@@ -15,8 +15,11 @@
*/
package com.google.android.exoplayer2.ext.vp9;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
+import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.util.LibraryLoader;
+import com.google.android.exoplayer2.util.Util;
/**
* Configures and queries the underlying native library.
@@ -27,7 +30,8 @@ public final class VpxLibrary {
ExoPlayerLibraryInfo.registerModule("goog.exo.vpx");
}
- private static final LibraryLoader LOADER = new LibraryLoader("vpx", "vpxJNI");
+ private static final LibraryLoader LOADER = new LibraryLoader("vpx", "vpxV2JNI");
+ @Nullable private static Class extends ExoMediaCrypto> exoMediaCryptoType;
private VpxLibrary() {}
@@ -36,10 +40,14 @@ public final class VpxLibrary {
* it must do so before calling any other method defined by this class, and before instantiating a
* {@link LibvpxVideoRenderer} instance.
*
+ * @param exoMediaCryptoType The {@link ExoMediaCrypto} type required for decoding protected
+ * content.
* @param libraries The names of the Vpx native libraries.
*/
- public static void setLibraries(String... libraries) {
+ public static void setLibraries(
+ Class extends ExoMediaCrypto> exoMediaCryptoType, String... libraries) {
LOADER.setLibraries(libraries);
+ VpxLibrary.exoMediaCryptoType = exoMediaCryptoType;
}
/**
@@ -49,9 +57,8 @@ public final class VpxLibrary {
return LOADER.isAvailable();
}
- /**
- * Returns the version of the underlying library if available, or null otherwise.
- */
+ /** Returns the version of the underlying library if available, or null otherwise. */
+ @Nullable
public static String getVersion() {
return isAvailable() ? vpxGetVersion() : null;
}
@@ -60,6 +67,7 @@ public final class VpxLibrary {
* Returns the configuration string with which the underlying library was built if available, or
* null otherwise.
*/
+ @Nullable
public static String getBuildConfig() {
return isAvailable() ? vpxGetBuildConfig() : null;
}
@@ -74,6 +82,15 @@ public final class VpxLibrary {
return indexHbd >= 0;
}
+ /**
+ * Returns whether the given {@link ExoMediaCrypto} type matches the one required for decoding
+ * protected content.
+ */
+ public static boolean matchesExpectedExoMediaCryptoType(
+ @Nullable Class extends ExoMediaCrypto> exoMediaCryptoType) {
+ return Util.areEqual(VpxLibrary.exoMediaCryptoType, exoMediaCryptoType);
+ }
+
private static native String vpxGetVersion();
private static native String vpxGetBuildConfig();
public static native boolean vpxIsSecureDecodeSupported();
diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java
index fa0df1cfa9..7177cde12e 100644
--- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java
+++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java
@@ -15,39 +15,12 @@
*/
package com.google.android.exoplayer2.ext.vp9;
-import com.google.android.exoplayer2.decoder.OutputBuffer;
-import com.google.android.exoplayer2.video.ColorInfo;
-import java.nio.ByteBuffer;
+import com.google.android.exoplayer2.video.VideoDecoderOutputBuffer;
-/**
- * Output buffer containing video frame data, populated by {@link VpxDecoder}.
- */
-/* package */ final class VpxOutputBuffer extends OutputBuffer {
-
- public static final int COLORSPACE_UNKNOWN = 0;
- public static final int COLORSPACE_BT601 = 1;
- public static final int COLORSPACE_BT709 = 2;
- public static final int COLORSPACE_BT2020 = 3;
+/** Video output buffer, populated by {@link VpxDecoder}. */
+public final class VpxOutputBuffer extends VideoDecoderOutputBuffer {
private final VpxDecoder owner;
- /** Decoder private data. */
- public int decoderPrivate;
-
- public int mode;
- /**
- * RGB buffer for RGB mode.
- */
- public ByteBuffer data;
- public int width;
- public int height;
- public ColorInfo colorInfo;
-
- /**
- * YUV planes for YUV mode.
- */
- public ByteBuffer[] yuvPlanes;
- public int[] yuvStrides;
- public int colorspace;
public VpxOutputBuffer(VpxDecoder owner) {
this.owner = owner;
@@ -58,91 +31,4 @@ import java.nio.ByteBuffer;
owner.releaseOutputBuffer(this);
}
- /**
- * Initializes the buffer.
- *
- * @param timeUs The presentation timestamp for the buffer, in microseconds.
- * @param mode The output mode. One of {@link VpxDecoder#OUTPUT_MODE_NONE},
- * {@link VpxDecoder#OUTPUT_MODE_RGB} and {@link VpxDecoder#OUTPUT_MODE_YUV}.
- */
- public void init(long timeUs, int mode) {
- this.timeUs = timeUs;
- this.mode = mode;
- }
-
- /**
- * Resizes the buffer based on the given dimensions. Called via JNI after decoding completes.
- * @return Whether the buffer was resized successfully.
- */
- public boolean initForRgbFrame(int width, int height) {
- this.width = width;
- this.height = height;
- this.yuvPlanes = null;
- if (!isSafeToMultiply(width, height) || !isSafeToMultiply(width * height, 2)) {
- return false;
- }
- int minimumRgbSize = width * height * 2;
- initData(minimumRgbSize);
- return true;
- }
-
- /**
- * Resizes the buffer based on the given stride. Called via JNI after decoding completes.
- * @return Whether the buffer was resized successfully.
- */
- public boolean initForYuvFrame(int width, int height, int yStride, int uvStride,
- int colorspace) {
- this.width = width;
- this.height = height;
- this.colorspace = colorspace;
- int uvHeight = (int) (((long) height + 1) / 2);
- if (!isSafeToMultiply(yStride, height) || !isSafeToMultiply(uvStride, uvHeight)) {
- return false;
- }
- int yLength = yStride * height;
- int uvLength = uvStride * uvHeight;
- int minimumYuvSize = yLength + (uvLength * 2);
- if (!isSafeToMultiply(uvLength, 2) || minimumYuvSize < yLength) {
- return false;
- }
- initData(minimumYuvSize);
-
- if (yuvPlanes == null) {
- yuvPlanes = new ByteBuffer[3];
- }
- // Rewrapping has to be done on every frame since the stride might have changed.
- yuvPlanes[0] = data.slice();
- yuvPlanes[0].limit(yLength);
- data.position(yLength);
- yuvPlanes[1] = data.slice();
- yuvPlanes[1].limit(uvLength);
- data.position(yLength + uvLength);
- yuvPlanes[2] = data.slice();
- yuvPlanes[2].limit(uvLength);
- if (yuvStrides == null) {
- yuvStrides = new int[3];
- }
- yuvStrides[0] = yStride;
- yuvStrides[1] = uvStride;
- yuvStrides[2] = uvStride;
- return true;
- }
-
- private void initData(int size) {
- if (data == null || data.capacity() < size) {
- data = ByteBuffer.allocateDirect(size);
- } else {
- data.position(0);
- data.limit(size);
- }
- }
-
- /**
- * Ensures that the result of multiplying individual numbers can fit into the size limit of an
- * integer.
- */
- private boolean isSafeToMultiply(int a, int b) {
- return a >= 0 && b >= 0 && !(b > 0 && a >= Integer.MAX_VALUE / b);
- }
-
}
diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxRenderer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxRenderer.java
index 837539593e..d82f5a6071 100644
--- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxRenderer.java
+++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxRenderer.java
@@ -17,8 +17,7 @@ package com.google.android.exoplayer2.ext.vp9;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
+import com.google.android.exoplayer2.util.GlUtil;
import java.nio.FloatBuffer;
import java.util.concurrent.atomic.AtomicReference;
import javax.microedition.khronos.egl.EGLConfig;
@@ -72,11 +71,8 @@ import javax.microedition.khronos.opengles.GL10;
+ " gl_FragColor = vec4(mColorConversion * yuv, 1.0);\n"
+ "}\n";
- private static final FloatBuffer TEXTURE_VERTICES = nativeFloatBuffer(
- -1.0f, 1.0f,
- -1.0f, -1.0f,
- 1.0f, 1.0f,
- 1.0f, -1.0f);
+ private static final FloatBuffer TEXTURE_VERTICES =
+ GlUtil.createBuffer(new float[] {-1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f, -1.0f});
private final int[] yuvTextures = new int[3];
private final AtomicReference pendingOutputBufferReference;
@@ -114,21 +110,7 @@ import javax.microedition.khronos.opengles.GL10;
@Override
public void onSurfaceCreated(GL10 unused, EGLConfig config) {
- // Create the GL program.
- program = GLES20.glCreateProgram();
-
- // Add the vertex and fragment shaders.
- addShader(GLES20.GL_VERTEX_SHADER, VERTEX_SHADER, program);
- addShader(GLES20.GL_FRAGMENT_SHADER, FRAGMENT_SHADER, program);
-
- // Link the GL program.
- GLES20.glLinkProgram(program);
- int[] result = new int[] {
- GLES20.GL_FALSE
- };
- result[0] = GLES20.GL_FALSE;
- GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, result, 0);
- abortUnless(result[0] == GLES20.GL_TRUE, GLES20.glGetProgramInfoLog(program));
+ program = GlUtil.compileProgram(VERTEX_SHADER, FRAGMENT_SHADER);
GLES20.glUseProgram(program);
int posLocation = GLES20.glGetAttribLocation(program, "in_pos");
GLES20.glEnableVertexAttribArray(posLocation);
@@ -136,11 +118,11 @@ import javax.microedition.khronos.opengles.GL10;
posLocation, 2, GLES20.GL_FLOAT, false, 0, TEXTURE_VERTICES);
texLocation = GLES20.glGetAttribLocation(program, "in_tc");
GLES20.glEnableVertexAttribArray(texLocation);
- checkNoGLES2Error();
+ GlUtil.checkGlError();
colorMatrixLocation = GLES20.glGetUniformLocation(program, "mColorConversion");
- checkNoGLES2Error();
+ GlUtil.checkGlError();
setupTextures();
- checkNoGLES2Error();
+ GlUtil.checkGlError();
}
@Override
@@ -191,11 +173,8 @@ import javax.microedition.khronos.opengles.GL10;
float crop = (float) outputBuffer.width / outputBuffer.yuvStrides[0];
// This buffer is consumed during each call to glDrawArrays. It needs to be a member variable
// rather than a local variable to ensure that it doesn't get garbage collected.
- textureCoords = nativeFloatBuffer(
- 0.0f, 0.0f,
- 0.0f, 1.0f,
- crop, 0.0f,
- crop, 1.0f);
+ textureCoords =
+ GlUtil.createBuffer(new float[] {0.0f, 0.0f, 0.0f, 1.0f, crop, 0.0f, crop, 1.0f});
GLES20.glVertexAttribPointer(
texLocation, 2, GLES20.GL_FLOAT, false, 0, textureCoords);
previousWidth = outputBuffer.width;
@@ -203,23 +182,7 @@ import javax.microedition.khronos.opengles.GL10;
}
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
- checkNoGLES2Error();
- }
-
- private void addShader(int type, String source, int program) {
- int[] result = new int[] {
- GLES20.GL_FALSE
- };
- int shader = GLES20.glCreateShader(type);
- GLES20.glShaderSource(shader, source);
- GLES20.glCompileShader(shader);
- GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, result, 0);
- abortUnless(result[0] == GLES20.GL_TRUE,
- GLES20.glGetShaderInfoLog(shader) + ", source: " + source);
- GLES20.glAttachShader(program, shader);
- GLES20.glDeleteShader(shader);
-
- checkNoGLES2Error();
+ GlUtil.checkGlError();
}
private void setupTextures() {
@@ -237,28 +200,6 @@ import javax.microedition.khronos.opengles.GL10;
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
}
- checkNoGLES2Error();
+ GlUtil.checkGlError();
}
-
- private void abortUnless(boolean condition, String msg) {
- if (!condition) {
- throw new RuntimeException(msg);
- }
- }
-
- private void checkNoGLES2Error() {
- int error = GLES20.glGetError();
- if (error != GLES20.GL_NO_ERROR) {
- throw new RuntimeException("GLES20 error: " + error);
- }
- }
-
- private static FloatBuffer nativeFloatBuffer(float... array) {
- FloatBuffer buffer = ByteBuffer.allocateDirect(array.length * 4).order(
- ByteOrder.nativeOrder()).asFloatBuffer();
- buffer.put(array);
- buffer.flip();
- return buffer;
- }
-
}
diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxVideoSurfaceView.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxVideoSurfaceView.java
index 8c765952e7..4e983cccc7 100644
--- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxVideoSurfaceView.java
+++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxVideoSurfaceView.java
@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.ext.vp9;
import android.content.Context;
import android.opengl.GLSurfaceView;
+import androidx.annotation.Nullable;
import android.util.AttributeSet;
/**
@@ -27,10 +28,10 @@ public class VpxVideoSurfaceView extends GLSurfaceView implements VpxOutputBuffe
private final VpxRenderer renderer;
public VpxVideoSurfaceView(Context context) {
- this(context, null);
+ this(context, /* attrs= */ null);
}
- public VpxVideoSurfaceView(Context context, AttributeSet attrs) {
+ public VpxVideoSurfaceView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
renderer = new VpxRenderer();
setPreserveEGLContextOnPause(true);
diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/package-info.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/package-info.java
new file mode 100644
index 0000000000..b8725607a5
--- /dev/null
+++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+@NonNullApi
+package com.google.android.exoplayer2.ext.vp9;
+
+import com.google.android.exoplayer2.util.NonNullApi;
diff --git a/extensions/vp9/src/main/jni/Android.mk b/extensions/vp9/src/main/jni/Android.mk
index 868b869d56..cb7571a1b0 100644
--- a/extensions/vp9/src/main/jni/Android.mk
+++ b/extensions/vp9/src/main/jni/Android.mk
@@ -17,27 +17,21 @@
WORKING_DIR := $(call my-dir)
include $(CLEAR_VARS)
LIBVPX_ROOT := $(WORKING_DIR)/libvpx
-LIBYUV_ROOT := $(WORKING_DIR)/libyuv
-
-# build libyuv_static.a
-LOCAL_PATH := $(WORKING_DIR)
-LIBYUV_DISABLE_JPEG := "yes"
-include $(LIBYUV_ROOT)/Android.mk
# build libvpx.so
LOCAL_PATH := $(WORKING_DIR)
include libvpx.mk
-# build libvpxJNI.so
+# build libvpxV2JNI.so
include $(CLEAR_VARS)
LOCAL_PATH := $(WORKING_DIR)
-LOCAL_MODULE := libvpxJNI
+LOCAL_MODULE := libvpxV2JNI
LOCAL_ARM_MODE := arm
LOCAL_CPP_EXTENSION := .cc
LOCAL_SRC_FILES := vpx_jni.cc
LOCAL_LDLIBS := -llog -lz -lm -landroid
LOCAL_SHARED_LIBRARIES := libvpx
-LOCAL_STATIC_LIBRARIES := libyuv_static cpufeatures
+LOCAL_STATIC_LIBRARIES := cpufeatures
include $(BUILD_SHARED_LIBRARY)
$(call import-module,android/cpufeatures)
diff --git a/extensions/vp9/src/main/jni/Application.mk b/extensions/vp9/src/main/jni/Application.mk
index 59bf5f8f87..ed28f07acb 100644
--- a/extensions/vp9/src/main/jni/Application.mk
+++ b/extensions/vp9/src/main/jni/Application.mk
@@ -15,6 +15,6 @@
#
APP_OPTIM := release
-APP_STL := gnustl_static
+APP_STL := c++_static
APP_CPPFLAGS := -frtti
-APP_PLATFORM := android-9
+APP_PLATFORM := android-16
diff --git a/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh b/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh
index eab6862555..18f1dd5c69 100755
--- a/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh
+++ b/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh
@@ -20,46 +20,33 @@
set -e
-if [ $# -ne 1 ]; then
- echo "Usage: ${0} "
+if [ $# -ne 0 ]; then
+ echo "Usage: ${0}"
exit
fi
-ndk="${1}"
-shift 1
-
# configuration parameters common to all architectures
common_params="--disable-examples --disable-docs --enable-realtime-only"
common_params+=" --disable-vp8 --disable-vp9-encoder --disable-webm-io"
common_params+=" --disable-libyuv --disable-runtime-cpu-detect"
+common_params+=" --enable-external-build"
# configuration parameters for various architectures
arch[0]="armeabi-v7a"
-config[0]="--target=armv7-android-gcc --sdk-path=$ndk --enable-neon"
-config[0]+=" --enable-neon-asm"
+config[0]="--target=armv7-android-gcc --enable-neon --enable-neon-asm"
-arch[1]="armeabi"
-config[1]="--target=armv7-android-gcc --sdk-path=$ndk --disable-neon"
-config[1]+=" --disable-neon-asm"
+arch[1]="x86"
+config[1]="--force-target=x86-android-gcc --disable-sse2"
+config[1]+=" --disable-sse3 --disable-ssse3 --disable-sse4_1 --disable-avx"
+config[1]+=" --disable-avx2 --enable-pic"
-arch[2]="mips"
-config[2]="--force-target=mips32-android-gcc --sdk-path=$ndk"
+arch[2]="arm64-v8a"
+config[2]="--force-target=armv8-android-gcc --enable-neon"
-arch[3]="x86"
-config[3]="--force-target=x86-android-gcc --sdk-path=$ndk --disable-sse2"
+arch[3]="x86_64"
+config[3]="--force-target=x86_64-android-gcc --disable-sse2"
config[3]+=" --disable-sse3 --disable-ssse3 --disable-sse4_1 --disable-avx"
-config[3]+=" --disable-avx2 --enable-pic"
-
-arch[4]="arm64-v8a"
-config[4]="--force-target=armv8-android-gcc --sdk-path=$ndk --enable-neon"
-
-arch[5]="x86_64"
-config[5]="--force-target=x86_64-android-gcc --sdk-path=$ndk --disable-sse2"
-config[5]+=" --disable-sse3 --disable-ssse3 --disable-sse4_1 --disable-avx"
-config[5]+=" --disable-avx2 --enable-pic --disable-neon --disable-neon-asm"
-
-arch[6]="mips64"
-config[6]="--force-target=mips64-android-gcc --sdk-path=$ndk"
+config[3]+=" --disable-avx2 --enable-pic --disable-neon --disable-neon-asm"
limit=$((${#arch[@]} - 1))
@@ -102,10 +89,7 @@ for i in $(seq 0 ${limit}); do
# configure and make
echo "build_android_configs: "
echo "configure ${config[${i}]} ${common_params}"
- ../../libvpx/configure ${config[${i}]} ${common_params} --extra-cflags=" \
- -isystem $ndk/sysroot/usr/include/arm-linux-androideabi \
- -isystem $ndk/sysroot/usr/include \
- "
+ ../../libvpx/configure ${config[${i}]} ${common_params}
rm -f libvpx_srcs.txt
for f in ${allowed_files}; do
# the build system supports multiple different configurations. avoid
diff --git a/extensions/vp9/src/main/jni/vpx_jni.cc b/extensions/vp9/src/main/jni/vpx_jni.cc
index f36c433b22..9fc8b09a18 100644
--- a/extensions/vp9/src/main/jni/vpx_jni.cc
+++ b/extensions/vp9/src/main/jni/vpx_jni.cc
@@ -30,8 +30,6 @@
#include
#include
-#include "libyuv.h" // NOLINT
-
#define VPX_CODEC_DISABLE_COMPAT 1
#include "vpx/vpx_decoder.h"
#include "vpx/vp8dx.h"
@@ -61,8 +59,8 @@
(JNIEnv* env, jobject thiz, ##__VA_ARGS__)\
// JNI references for VpxOutputBuffer class.
-static jmethodID initForRgbFrame;
static jmethodID initForYuvFrame;
+static jmethodID initForPrivateFrame;
static jfieldID dataField;
static jfieldID outputModeField;
static jfieldID decoderPrivateField;
@@ -393,11 +391,7 @@ class JniBufferManager {
};
struct JniCtx {
- JniCtx(bool enableBufferManager) {
- if (enableBufferManager) {
- buffer_manager = new JniBufferManager();
- }
- }
+ JniCtx() { buffer_manager = new JniBufferManager(); }
~JniCtx() {
if (native_window) {
@@ -441,11 +435,11 @@ int vpx_release_frame_buffer(void* priv, vpx_codec_frame_buffer_t* fb) {
}
DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter,
- jboolean enableBufferManager) {
- JniCtx* context = new JniCtx(enableBufferManager);
+ jboolean enableRowMultiThreadMode, jint threads) {
+ JniCtx* context = new JniCtx();
context->decoder = new vpx_codec_ctx_t();
vpx_codec_dec_cfg_t cfg = {0, 0, 0};
- cfg.threads = android_getCpuCount();
+ cfg.threads = threads;
errorCode = 0;
vpx_codec_err_t err =
vpx_codec_dec_init(context->decoder, &vpx_codec_vp9_dx_algo, &cfg, 0);
@@ -454,21 +448,33 @@ DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter,
errorCode = err;
return 0;
}
+#ifdef VPX_CTRL_VP9_DECODE_SET_ROW_MT
+ err = vpx_codec_control(context->decoder, VP9D_SET_ROW_MT,
+ enableRowMultiThreadMode);
+ if (err) {
+ LOGE("ERROR: Failed to enable row multi thread mode, error = %d.", err);
+ }
+#endif
if (disableLoopFilter) {
- // TODO(b/71930387): Use vpx_codec_control(), not vpx_codec_control_().
- err = vpx_codec_control_(context->decoder, VP9_SET_SKIP_LOOP_FILTER, true);
+ err = vpx_codec_control(context->decoder, VP9_SET_SKIP_LOOP_FILTER, true);
if (err) {
LOGE("ERROR: Failed to shut off libvpx loop filter, error = %d.", err);
}
- }
- if (enableBufferManager) {
- err = vpx_codec_set_frame_buffer_functions(
- context->decoder, vpx_get_frame_buffer, vpx_release_frame_buffer,
- context->buffer_manager);
+#ifdef VPX_CTRL_VP9_SET_LOOP_FILTER_OPT
+ } else {
+ err = vpx_codec_control(context->decoder, VP9D_SET_LOOP_FILTER_OPT, true);
if (err) {
- LOGE("ERROR: Failed to set libvpx frame buffer functions, error = %d.",
+ LOGE("ERROR: Failed to enable loop filter optimization, error = %d.",
err);
}
+#endif
+ }
+ err = vpx_codec_set_frame_buffer_functions(
+ context->decoder, vpx_get_frame_buffer, vpx_release_frame_buffer,
+ context->buffer_manager);
+ if (err) {
+ LOGE("ERROR: Failed to set libvpx frame buffer functions, error = %d.",
+ err);
}
// Populate JNI References.
@@ -476,8 +482,8 @@ DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter,
"com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer");
initForYuvFrame = env->GetMethodID(outputBufferClass, "initForYuvFrame",
"(IIIII)Z");
- initForRgbFrame = env->GetMethodID(outputBufferClass, "initForRgbFrame",
- "(II)Z");
+ initForPrivateFrame =
+ env->GetMethodID(outputBufferClass, "initForPrivateFrame", "(II)V");
dataField = env->GetFieldID(outputBufferClass, "data",
"Ljava/nio/ByteBuffer;");
outputModeField = env->GetFieldID(outputBufferClass, "mode", "I");
@@ -529,28 +535,10 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) {
}
const int kOutputModeYuv = 0;
- const int kOutputModeRgb = 1;
- const int kOutputModeSurfaceYuv = 2;
+ const int kOutputModeSurfaceYuv = 1;
int outputMode = env->GetIntField(jOutputBuffer, outputModeField);
- if (outputMode == kOutputModeRgb) {
- // resize buffer if required.
- jboolean initResult = env->CallBooleanMethod(jOutputBuffer, initForRgbFrame,
- img->d_w, img->d_h);
- if (env->ExceptionCheck() || !initResult) {
- return -1;
- }
-
- // get pointer to the data buffer.
- const jobject dataObject = env->GetObjectField(jOutputBuffer, dataField);
- uint8_t* const dst =
- reinterpret_cast(env->GetDirectBufferAddress(dataObject));
-
- libyuv::I420ToRGB565(img->planes[VPX_PLANE_Y], img->stride[VPX_PLANE_Y],
- img->planes[VPX_PLANE_U], img->stride[VPX_PLANE_U],
- img->planes[VPX_PLANE_V], img->stride[VPX_PLANE_V],
- dst, img->d_w * 2, img->d_w, img->d_h);
- } else if (outputMode == kOutputModeYuv) {
+ if (outputMode == kOutputModeYuv) {
const int kColorspaceUnknown = 0;
const int kColorspaceBT601 = 1;
const int kColorspaceBT709 = 2;
@@ -608,9 +596,6 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) {
}
} else if (outputMode == kOutputModeSurfaceYuv &&
img->fmt != VPX_IMG_FMT_I42016) {
- if (!context->buffer_manager) {
- return -1; // enableBufferManager was not set in vpxInit.
- }
int id = *(int*)img->fb_priv;
context->buffer_manager->add_ref(id);
JniFrameBuffer* jfb = context->buffer_manager->get_buffer(id);
@@ -620,6 +605,10 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) {
}
jfb->d_w = img->d_w;
jfb->d_h = img->d_h;
+ env->CallVoidMethod(jOutputBuffer, initForPrivateFrame, img->d_w, img->d_h);
+ if (env->ExceptionCheck()) {
+ return -1;
+ }
env->SetIntField(jOutputBuffer, decoderPrivateField,
id + kDecoderPrivateBase);
}
diff --git a/extensions/vp9/src/test/AndroidManifest.xml b/extensions/vp9/src/test/AndroidManifest.xml
new file mode 100644
index 0000000000..851213e653
--- /dev/null
+++ b/extensions/vp9/src/test/AndroidManifest.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
diff --git a/extensions/vp9/src/test/java/com/google/android/exoplayer2/ext/vp9/DefaultRenderersFactoryTest.java b/extensions/vp9/src/test/java/com/google/android/exoplayer2/ext/vp9/DefaultRenderersFactoryTest.java
new file mode 100644
index 0000000000..33de600aa7
--- /dev/null
+++ b/extensions/vp9/src/test/java/com/google/android/exoplayer2/ext/vp9/DefaultRenderersFactoryTest.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2019 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.vp9;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.testutil.DefaultRenderersFactoryAsserts;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit test for {@link DefaultRenderersFactoryTest} with {@link LibvpxVideoRenderer}. */
+@RunWith(AndroidJUnit4.class)
+public final class DefaultRenderersFactoryTest {
+
+ @Test
+ public void createRenderers_instantiatesVpxRenderer() {
+ DefaultRenderersFactoryAsserts.assertExtensionRendererCreated(
+ LibvpxVideoRenderer.class, C.TRACK_TYPE_VIDEO);
+ }
+}
diff --git a/extensions/workmanager/README.md b/extensions/workmanager/README.md
new file mode 100644
index 0000000000..bd2dbc71ad
--- /dev/null
+++ b/extensions/workmanager/README.md
@@ -0,0 +1,22 @@
+# ExoPlayer WorkManager extension
+
+This extension provides a Scheduler implementation which uses [WorkManager][].
+
+[WorkManager]: https://developer.android.com/topic/libraries/architecture/workmanager.html
+
+## 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-workmanager: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/workmanager/build.gradle b/extensions/workmanager/build.gradle
new file mode 100644
index 0000000000..ea7564316f
--- /dev/null
+++ b/extensions/workmanager/build.gradle
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2019 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
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ defaultConfig {
+ minSdkVersion project.ext.minSdkVersion
+ targetSdkVersion project.ext.targetSdkVersion
+ }
+
+ testOptions.unitTests.includeAndroidResources = true
+}
+
+dependencies {
+ implementation project(modulePrefix + 'library-core')
+ implementation 'androidx.work:work-runtime:2.1.0'
+}
+
+ext {
+ javadocTitle = 'WorkManager extension'
+}
+apply from: '../../javadoc_library.gradle'
+
+ext {
+ releaseArtifact = 'extension-workmanager'
+ releaseDescription = 'WorkManager extension for ExoPlayer.'
+}
+apply from: '../../publish.gradle'
diff --git a/extensions/workmanager/src/main/AndroidManifest.xml b/extensions/workmanager/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..1daf50bd00
--- /dev/null
+++ b/extensions/workmanager/src/main/AndroidManifest.xml
@@ -0,0 +1,18 @@
+
+
+
+
diff --git a/extensions/workmanager/src/main/java/com/google/android/exoplayer2/ext/workmanager/WorkManagerScheduler.java b/extensions/workmanager/src/main/java/com/google/android/exoplayer2/ext/workmanager/WorkManagerScheduler.java
new file mode 100644
index 0000000000..01801c9897
--- /dev/null
+++ b/extensions/workmanager/src/main/java/com/google/android/exoplayer2/ext/workmanager/WorkManagerScheduler.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2019 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.workmanager;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.Intent;
+import androidx.work.Constraints;
+import androidx.work.Data;
+import androidx.work.ExistingWorkPolicy;
+import androidx.work.NetworkType;
+import androidx.work.OneTimeWorkRequest;
+import androidx.work.WorkManager;
+import androidx.work.Worker;
+import androidx.work.WorkerParameters;
+import com.google.android.exoplayer2.scheduler.Requirements;
+import com.google.android.exoplayer2.scheduler.Scheduler;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Log;
+import com.google.android.exoplayer2.util.Util;
+
+/** A {@link Scheduler} that uses {@link WorkManager}. */
+public final class WorkManagerScheduler implements Scheduler {
+
+ private static final boolean DEBUG = false;
+ private static final String TAG = "WorkManagerScheduler";
+ 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 workName;
+
+ /**
+ * @param workName A name for work scheduled by this instance. If the same name 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 WorkManagerScheduler(String workName) {
+ this.workName = workName;
+ }
+
+ @Override
+ public boolean schedule(Requirements requirements, String servicePackage, String serviceAction) {
+ Constraints constraints = buildConstraints(requirements);
+ Data inputData = buildInputData(requirements, servicePackage, serviceAction);
+ OneTimeWorkRequest workRequest = buildWorkRequest(constraints, inputData);
+ logd("Scheduling work: " + workName);
+ WorkManager.getInstance().enqueueUniqueWork(workName, ExistingWorkPolicy.REPLACE, workRequest);
+ return true;
+ }
+
+ @Override
+ public boolean cancel() {
+ logd("Canceling work: " + workName);
+ WorkManager.getInstance().cancelUniqueWork(workName);
+ return true;
+ }
+
+ private static Constraints buildConstraints(Requirements requirements) {
+ Constraints.Builder builder = new Constraints.Builder();
+
+ if (requirements.isUnmeteredNetworkRequired()) {
+ builder.setRequiredNetworkType(NetworkType.UNMETERED);
+ } else if (requirements.isNetworkRequired()) {
+ builder.setRequiredNetworkType(NetworkType.CONNECTED);
+ } else {
+ builder.setRequiredNetworkType(NetworkType.NOT_REQUIRED);
+ }
+
+ if (requirements.isChargingRequired()) {
+ builder.setRequiresCharging(true);
+ }
+
+ if (requirements.isIdleRequired() && Util.SDK_INT >= 23) {
+ setRequiresDeviceIdle(builder);
+ }
+
+ return builder.build();
+ }
+
+ @TargetApi(23)
+ private static void setRequiresDeviceIdle(Constraints.Builder builder) {
+ builder.setRequiresDeviceIdle(true);
+ }
+
+ private static Data buildInputData(
+ Requirements requirements, String servicePackage, String serviceAction) {
+ Data.Builder builder = new Data.Builder();
+
+ builder.putInt(KEY_REQUIREMENTS, requirements.getRequirements());
+ builder.putString(KEY_SERVICE_PACKAGE, servicePackage);
+ builder.putString(KEY_SERVICE_ACTION, serviceAction);
+
+ return builder.build();
+ }
+
+ private static OneTimeWorkRequest buildWorkRequest(Constraints constraints, Data inputData) {
+ OneTimeWorkRequest.Builder builder = new OneTimeWorkRequest.Builder(SchedulerWorker.class);
+
+ builder.setConstraints(constraints);
+ builder.setInputData(inputData);
+
+ return builder.build();
+ }
+
+ private static void logd(String message) {
+ if (DEBUG) {
+ Log.d(TAG, message);
+ }
+ }
+
+ /** A {@link Worker} that starts the target service if the requirements are met. */
+ // This class needs to be public so that WorkManager can instantiate it.
+ public static final class SchedulerWorker extends Worker {
+
+ private final WorkerParameters workerParams;
+ private final Context context;
+
+ public SchedulerWorker(Context context, WorkerParameters workerParams) {
+ super(context, workerParams);
+ this.workerParams = workerParams;
+ this.context = context;
+ }
+
+ @Override
+ public Result doWork() {
+ logd("SchedulerWorker is started");
+ Data inputData = workerParams.getInputData();
+ Assertions.checkNotNull(inputData, "Work started without input data.");
+ Requirements requirements = new Requirements(inputData.getInt(KEY_REQUIREMENTS, 0));
+ if (requirements.checkRequirements(context)) {
+ logd("Requirements are met");
+ String serviceAction = inputData.getString(KEY_SERVICE_ACTION);
+ String servicePackage = inputData.getString(KEY_SERVICE_PACKAGE);
+ Assertions.checkNotNull(serviceAction, "Service action missing.");
+ Assertions.checkNotNull(servicePackage, "Service package missing.");
+ Intent intent = new Intent(serviceAction).setPackage(servicePackage);
+ logd("Starting service action: " + serviceAction + " package: " + servicePackage);
+ Util.startForegroundService(context, intent);
+ return Result.success();
+ } else {
+ logd("Requirements are not met");
+ return Result.retry();
+ }
+ }
+ }
+}
diff --git a/extensions/workmanager/src/main/java/com/google/android/exoplayer2/ext/workmanager/package-info.java b/extensions/workmanager/src/main/java/com/google/android/exoplayer2/ext/workmanager/package-info.java
new file mode 100644
index 0000000000..7e0e244231
--- /dev/null
+++ b/extensions/workmanager/src/main/java/com/google/android/exoplayer2/ext/workmanager/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+@NonNullApi
+package com.google.android.exoplayer2.ext.workmanager;
+
+import com.google.android.exoplayer2.util.NonNullApi;
diff --git a/gradle.properties b/gradle.properties
index b55575bc3b..31ff0ad6b6 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,3 +1,6 @@
## Project-wide Gradle settings.
-android.useDeprecatedNdk=true
+android.useAndroidX=true
+android.enableJetifier=true
+android.enableUnitTestBinaryResources=true
buildDir=buildout
+org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 5559e8ccfa..6d00e1ce97 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Tue Sep 05 13:43:42 BST 2017
+#Thu Apr 25 13:15:25 BST 2019
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip
diff --git a/javadoc_combined.gradle b/javadoc_combined.gradle
index 209ad3a1a3..d2fa241a81 100644
--- a/javadoc_combined.gradle
+++ b/javadoc_combined.gradle
@@ -42,7 +42,7 @@ class CombinedJavadocPlugin implements Plugin {
if (name == "release") {
classpath +=
libraryModule.project.files(
- variant.javaCompile.classpath.files,
+ variant.javaCompileProvider.get().classpath.files,
libraryModule.project.android.getBootClasspath())
}
}
diff --git a/javadoc_library.gradle b/javadoc_library.gradle
index 65219843e3..74fcc3dd6c 100644
--- a/javadoc_library.gradle
+++ b/javadoc_library.gradle
@@ -18,10 +18,13 @@ android.libraryVariants.all { variant ->
if (!name.equals("release")) {
return; // Skip non-release builds.
}
+ def allSourceDirs = variant.sourceSets.inject ([]) {
+ acc, val -> acc << val.javaDirectories
+ }
task("generateJavadoc", type: Javadoc) {
description = "Generates Javadoc for the ${javadocTitle}."
title = "ExoPlayer ${javadocTitle}"
- source = variant.javaCompile.source
+ source = allSourceDirs
options {
links "http://docs.oracle.com/javase/7/docs/api/"
linksOffline "https://developer.android.com/reference",
@@ -33,7 +36,7 @@ android.libraryVariants.all { variant ->
doFirst {
classpath =
files(
- variant.javaCompile.classpath.files,
+ variant.javaCompileProvider.get().classpath.files,
project.android.getBootClasspath())
}
doLast {
diff --git a/library/all/README.md b/library/all/README.md
index 8746e3afc6..43f942116e 100644
--- a/library/all/README.md
+++ b/library/all/README.md
@@ -10,4 +10,4 @@ individually. See ExoPlayer's [top level README][] for more information.
* [Javadoc][]: Note that this Javadoc is combined with that of other modules.
-[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
+[Javadoc]: https://exoplayer.dev/doc/reference/index.html
diff --git a/library/all/build.gradle b/library/all/build.gradle
index bb832ba0ff..f78b8b2132 100644
--- a/library/all/build.gradle
+++ b/library/all/build.gradle
@@ -16,7 +16,6 @@ apply plugin: 'com.android.library'
android {
compileSdkVersion project.ext.compileSdkVersion
- buildToolsVersion project.ext.buildToolsVersion
defaultConfig {
minSdkVersion project.ext.minSdkVersion
diff --git a/library/core/README.md b/library/core/README.md
index f31ffed131..7fa89dda8d 100644
--- a/library/core/README.md
+++ b/library/core/README.md
@@ -6,4 +6,4 @@ The core of the ExoPlayer library.
* [Javadoc][]: Note that this Javadoc is combined with that of other modules.
-[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
+[Javadoc]: https://exoplayer.dev/doc/reference/index.html
diff --git a/library/core/build.gradle b/library/core/build.gradle
index 606033fdea..fda2f079de 100644
--- a/library/core/build.gradle
+++ b/library/core/build.gradle
@@ -16,7 +16,6 @@ apply from: '../../constants.gradle'
android {
compileSdkVersion project.ext.compileSdkVersion
- buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@@ -43,7 +42,6 @@ android {
}
test {
java.srcDirs += '../../testutils/src/main/java/'
- java.srcDirs += '../../testutils_robolectric/src/main/java/'
}
}
@@ -54,21 +52,28 @@ android {
// testCoverageEnabled = true
// }
}
+
+ testOptions.unitTests.includeAndroidResources = true
}
dependencies {
- implementation 'com.android.support:support-annotations:' + supportLibraryVersion
+ implementation 'androidx.annotation:annotation:1.1.0'
+ compileOnly 'com.google.code.findbugs:jsr305:' + jsr305Version
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion
- androidTestImplementation 'androidx.test:runner:' + testRunnerVersion
- androidTestImplementation 'com.google.auto.value:auto-value-annotations:' + autoValueVersion
- androidTestImplementation 'com.google.dexmaker:dexmaker:' + dexmakerVersion
- androidTestImplementation 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion
+ // Uncomment to enable Kotlin non-null strict mode. See [internal: b/138703808].
+ // compileOnly "org.jetbrains.kotlin:kotlin-annotations-jvm:1.1.60"
+ androidTestImplementation 'androidx.test:runner:' + androidXTestVersion
+ androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion
androidTestImplementation 'com.google.truth:truth:' + truthVersion
+ androidTestImplementation 'com.google.auto.value:auto-value-annotations:' + autoValueVersion
+ androidTestImplementation 'com.linkedin.dexmaker:dexmaker:' + dexmakerVersion
+ androidTestImplementation 'com.linkedin.dexmaker:dexmaker-mockito:' + dexmakerVersion
androidTestImplementation 'org.mockito:mockito-core:' + mockitoVersion
androidTestAnnotationProcessor 'com.google.auto.value:auto-value:' + autoValueVersion
+ testImplementation 'androidx.test:core:' + androidXTestVersion
+ testImplementation 'androidx.test.ext:junit:' + androidXTestVersion
testImplementation 'com.google.truth:truth:' + truthVersion
- testImplementation 'junit:junit:' + junitVersion
testImplementation 'org.mockito:mockito-core:' + mockitoVersion
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
testImplementation 'com.google.auto.value:auto-value-annotations:' + autoValueVersion
diff --git a/library/core/proguard-rules.txt b/library/core/proguard-rules.txt
index fe204822a8..1f7a8d0ee7 100644
--- a/library/core/proguard-rules.txt
+++ b/library/core/proguard-rules.txt
@@ -3,7 +3,7 @@
# Constructors accessed via reflection in DefaultRenderersFactory
-dontnote com.google.android.exoplayer2.ext.vp9.LibvpxVideoRenderer
-keepclassmembers class com.google.android.exoplayer2.ext.vp9.LibvpxVideoRenderer {
- (boolean, long, android.os.Handler, com.google.android.exoplayer2.video.VideoRendererEventListener, int);
+ (long, android.os.Handler, com.google.android.exoplayer2.video.VideoRendererEventListener, int);
}
-dontnote com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer
-keepclassmembers class com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer {
@@ -30,5 +30,33 @@
();
}
+# Constructors accessed via reflection in DefaultDownloaderFactory
+-dontnote com.google.android.exoplayer2.source.dash.offline.DashDownloader
+-keepclassmembers class com.google.android.exoplayer2.source.dash.offline.DashDownloader {
+ (android.net.Uri, java.util.List, com.google.android.exoplayer2.offline.DownloaderConstructorHelper);
+}
+-dontnote com.google.android.exoplayer2.source.hls.offline.HlsDownloader
+-keepclassmembers class com.google.android.exoplayer2.source.hls.offline.HlsDownloader {
+ (android.net.Uri, java.util.List, com.google.android.exoplayer2.offline.DownloaderConstructorHelper);
+}
+-dontnote com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloader
+-keepclassmembers class com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloader {
+ (android.net.Uri, java.util.List, com.google.android.exoplayer2.offline.DownloaderConstructorHelper);
+}
+
+# Constructors accessed via reflection in DownloadHelper
+-dontnote com.google.android.exoplayer2.source.dash.DashMediaSource$Factory
+-keepclasseswithmembers class com.google.android.exoplayer2.source.dash.DashMediaSource$Factory {
+ (com.google.android.exoplayer2.upstream.DataSource$Factory);
+}
+-dontnote com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory
+-keepclasseswithmembers class com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory {
+ (com.google.android.exoplayer2.upstream.DataSource$Factory);
+}
+-dontnote com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory
+-keepclasseswithmembers class com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory {
+ (com.google.android.exoplayer2.upstream.DataSource$Factory);
+}
+
# 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 d9104b1077..e6e874a27a 100644
--- a/library/core/src/androidTest/AndroidManifest.xml
+++ b/library/core/src/androidTest/AndroidManifest.xml
@@ -18,6 +18,9 @@
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/ContentDataSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java
index 45b784e30f..a76b5cf6c1 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java
@@ -26,10 +26,10 @@ import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.test.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.FileNotFoundException;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/BasePlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/BasePlayer.java
new file mode 100644
index 0000000000..bb14ac147b
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/BasePlayer.java
@@ -0,0 +1,200 @@
+/*
+ * 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;
+
+import androidx.annotation.Nullable;
+import com.google.android.exoplayer2.util.Util;
+
+/** Abstract base {@link Player} which implements common implementation independent methods. */
+public abstract class BasePlayer implements Player {
+
+ protected final Timeline.Window window;
+
+ public BasePlayer() {
+ window = new Timeline.Window();
+ }
+
+ @Override
+ public final void seekToDefaultPosition() {
+ seekToDefaultPosition(getCurrentWindowIndex());
+ }
+
+ @Override
+ public final void seekToDefaultPosition(int windowIndex) {
+ seekTo(windowIndex, /* positionMs= */ C.TIME_UNSET);
+ }
+
+ @Override
+ public final void seekTo(long positionMs) {
+ seekTo(getCurrentWindowIndex(), positionMs);
+ }
+
+ @Override
+ public final boolean hasPrevious() {
+ return getPreviousWindowIndex() != C.INDEX_UNSET;
+ }
+
+ @Override
+ public final void previous() {
+ int previousWindowIndex = getPreviousWindowIndex();
+ if (previousWindowIndex != C.INDEX_UNSET) {
+ seekToDefaultPosition(previousWindowIndex);
+ }
+ }
+
+ @Override
+ public final boolean hasNext() {
+ return getNextWindowIndex() != C.INDEX_UNSET;
+ }
+
+ @Override
+ public final void next() {
+ int nextWindowIndex = getNextWindowIndex();
+ if (nextWindowIndex != C.INDEX_UNSET) {
+ seekToDefaultPosition(nextWindowIndex);
+ }
+ }
+
+ @Override
+ public final void stop() {
+ stop(/* reset= */ false);
+ }
+
+ @Override
+ public final int getNextWindowIndex() {
+ Timeline timeline = getCurrentTimeline();
+ return timeline.isEmpty()
+ ? C.INDEX_UNSET
+ : timeline.getNextWindowIndex(
+ getCurrentWindowIndex(), getRepeatModeForNavigation(), getShuffleModeEnabled());
+ }
+
+ @Override
+ public final int getPreviousWindowIndex() {
+ Timeline timeline = getCurrentTimeline();
+ return timeline.isEmpty()
+ ? C.INDEX_UNSET
+ : timeline.getPreviousWindowIndex(
+ getCurrentWindowIndex(), getRepeatModeForNavigation(), getShuffleModeEnabled());
+ }
+
+ @Override
+ @Nullable
+ public final Object getCurrentTag() {
+ Timeline timeline = getCurrentTimeline();
+ return timeline.isEmpty()
+ ? null
+ : timeline.getWindow(getCurrentWindowIndex(), window, /* setTag= */ true).tag;
+ }
+
+ @Override
+ @Nullable
+ public final Object getCurrentManifest() {
+ Timeline timeline = getCurrentTimeline();
+ return timeline.isEmpty()
+ ? null
+ : timeline.getWindow(getCurrentWindowIndex(), window, /* setTag= */ false).manifest;
+ }
+
+ @Override
+ public final int getBufferedPercentage() {
+ long position = getBufferedPosition();
+ long duration = getDuration();
+ return position == C.TIME_UNSET || duration == C.TIME_UNSET
+ ? 0
+ : duration == 0 ? 100 : Util.constrainValue((int) ((position * 100) / duration), 0, 100);
+ }
+
+ @Override
+ public final boolean isCurrentWindowDynamic() {
+ Timeline timeline = getCurrentTimeline();
+ return !timeline.isEmpty() && timeline.getWindow(getCurrentWindowIndex(), window).isDynamic;
+ }
+
+ @Override
+ public final boolean isCurrentWindowSeekable() {
+ Timeline timeline = getCurrentTimeline();
+ return !timeline.isEmpty() && timeline.getWindow(getCurrentWindowIndex(), window).isSeekable;
+ }
+
+ @Override
+ public final long getContentDuration() {
+ Timeline timeline = getCurrentTimeline();
+ return timeline.isEmpty()
+ ? C.TIME_UNSET
+ : timeline.getWindow(getCurrentWindowIndex(), window).getDurationMs();
+ }
+
+ @RepeatMode
+ private int getRepeatModeForNavigation() {
+ @RepeatMode int repeatMode = getRepeatMode();
+ return repeatMode == REPEAT_MODE_ONE ? REPEAT_MODE_OFF : repeatMode;
+ }
+
+ /** Holds a listener reference. */
+ protected static final class ListenerHolder {
+
+ /**
+ * The listener on which {link #invoke} will execute {@link ListenerInvocation listener
+ * invocations}.
+ */
+ public final Player.EventListener listener;
+
+ private boolean released;
+
+ public ListenerHolder(Player.EventListener listener) {
+ this.listener = listener;
+ }
+
+ /** Prevents any further {@link ListenerInvocation} to be executed on {@link #listener}. */
+ public void release() {
+ released = true;
+ }
+
+ /**
+ * Executes the given {@link ListenerInvocation} on {@link #listener}. Does nothing if {@link
+ * #release} has been called on this instance.
+ */
+ public void invoke(ListenerInvocation listenerInvocation) {
+ if (!released) {
+ listenerInvocation.invokeListener(listener);
+ }
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (other == null || getClass() != other.getClass()) {
+ return false;
+ }
+ return listener.equals(((ListenerHolder) other).listener);
+ }
+
+ @Override
+ public int hashCode() {
+ return listener.hashCode();
+ }
+ }
+
+ /** Parameterized invocation of a {@link Player.EventListener} method. */
+ protected interface ListenerInvocation {
+
+ /** Executes the invocation on the given {@link Player.EventListener}. */
+ void invokeListener(Player.EventListener listener);
+ }
+}
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 cb917b9b79..1099b14bfc 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
@@ -15,7 +15,7 @@
*/
package com.google.android.exoplayer2;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.drm.DrmInitData;
import com.google.android.exoplayer2.drm.DrmSessionManager;
@@ -37,7 +37,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
private SampleStream stream;
private Format[] streamFormats;
private long streamOffsetUs;
- private boolean readEndOfStream;
+ private long readingPositionUs;
private boolean streamIsFinal;
/**
@@ -46,7 +46,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
*/
public BaseRenderer(int trackType) {
this.trackType = trackType;
- readEndOfStream = true;
+ readingPositionUs = C.TIME_END_OF_SOURCE;
}
@Override
@@ -98,7 +98,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
throws ExoPlaybackException {
Assertions.checkState(!streamIsFinal);
this.stream = stream;
- readEndOfStream = false;
+ readingPositionUs = offsetUs;
streamFormats = formats;
streamOffsetUs = offsetUs;
onStreamChanged(formats, offsetUs);
@@ -111,7 +111,12 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
@Override
public final boolean hasReadStreamToEnd() {
- return readEndOfStream;
+ return readingPositionUs == C.TIME_END_OF_SOURCE;
+ }
+
+ @Override
+ public final long getReadingPositionUs() {
+ return readingPositionUs;
}
@Override
@@ -132,7 +137,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
@Override
public final void resetPosition(long positionUs) throws ExoPlaybackException {
streamIsFinal = false;
- readEndOfStream = false;
+ readingPositionUs = positionUs;
onPositionReset(positionUs, false);
}
@@ -153,6 +158,12 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
onDisabled();
}
+ @Override
+ public final void reset() {
+ Assertions.checkState(state == STATE_DISABLED);
+ onReset();
+ }
+
// RendererCapabilities implementation.
@Override
@@ -163,7 +174,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
// PlayerMessage.Target implementation.
@Override
- public void handleMessage(int what, Object object) throws ExoPlaybackException {
+ public void handleMessage(int what, @Nullable Object object) throws ExoPlaybackException {
// Do nothing.
}
@@ -247,6 +258,15 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
// Do nothing.
}
+ /**
+ * Called when the renderer is reset.
+ *
+ * The default implementation is a no-op.
+ */
+ protected void onReset() {
+ // Do nothing.
+ }
+
// Methods to be called by subclasses.
/** Returns the formats of the currently enabled stream. */
@@ -288,10 +308,11 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
int result = stream.readData(formatHolder, buffer, formatRequired);
if (result == C.RESULT_BUFFER_READ) {
if (buffer.isEndOfStream()) {
- readEndOfStream = true;
+ readingPositionUs = C.TIME_END_OF_SOURCE;
return streamIsFinal ? C.RESULT_BUFFER_READ : C.RESULT_NOTHING_READ;
}
buffer.timeUs += streamOffsetUs;
+ readingPositionUs = Math.max(readingPositionUs, buffer.timeUs);
} else if (result == C.RESULT_FORMAT_READ) {
Format format = formatHolder.format;
if (format.subsampleOffsetUs != Format.OFFSET_SAMPLE_RELATIVE) {
@@ -317,7 +338,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
* Returns whether the upstream source is ready.
*/
protected final boolean isSourceReady() {
- return readEndOfStream ? streamIsFinal : stream.isReady();
+ return hasReadStreamToEnd() ? streamIsFinal : stream.isReady();
}
/**
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 0cbdc14b1c..daa6124df6 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
@@ -21,13 +21,14 @@ import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.MediaCodec;
import android.media.MediaFormat;
-import android.support.annotation.IntDef;
+import androidx.annotation.IntDef;
import android.view.Surface;
import com.google.android.exoplayer2.PlayerMessage.Target;
import com.google.android.exoplayer2.audio.AuxEffectInfo;
import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.VideoFrameMetadataListener;
import com.google.android.exoplayer2.video.spherical.CameraMotionListener;
+import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.UUID;
@@ -70,9 +71,10 @@ public final class C {
/** Represents an unset or unknown percentage. */
public static final int PERCENTAGE_UNSET = -1;
- /**
- * The number of microseconds in one second.
- */
+ /** The number of milliseconds in one second. */
+ public static final long MILLIS_PER_SECOND = 1000L;
+
+ /** The number of microseconds in one second. */
public static final long MICROS_PER_SECOND = 1000000L;
/**
@@ -100,6 +102,9 @@ public final class C {
*/
public static final String UTF16_NAME = "UTF-16";
+ /** The name of the UTF-16 little-endian charset. */
+ public static final String UTF16LE_NAME = "UTF-16LE";
+
/**
* The name of the serif font family.
*/
@@ -114,6 +119,7 @@ public final class C {
* Crypto modes for a codec. One of {@link #CRYPTO_MODE_UNENCRYPTED}, {@link #CRYPTO_MODE_AES_CTR}
* or {@link #CRYPTO_MODE_AES_CBC}.
*/
+ @Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({CRYPTO_MODE_UNENCRYPTED, CRYPTO_MODE_AES_CTR, CRYPTO_MODE_AES_CBC})
public @interface CryptoMode {}
@@ -141,9 +147,10 @@ public final class C {
* {@link #ENCODING_INVALID}, {@link #ENCODING_PCM_8BIT}, {@link #ENCODING_PCM_16BIT}, {@link
* #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT}, {@link #ENCODING_PCM_FLOAT}, {@link
* #ENCODING_PCM_MU_LAW}, {@link #ENCODING_PCM_A_LAW}, {@link #ENCODING_AC3}, {@link
- * #ENCODING_E_AC3}, {@link #ENCODING_DTS}, {@link #ENCODING_DTS_HD} or {@link
- * #ENCODING_DOLBY_TRUEHD}.
+ * #ENCODING_E_AC3}, {@link #ENCODING_E_AC3_JOC}, {@link #ENCODING_AC4}, {@link #ENCODING_DTS},
+ * {@link #ENCODING_DTS_HD} or {@link #ENCODING_DOLBY_TRUEHD}.
*/
+ @Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({
Format.NO_VALUE,
@@ -157,9 +164,11 @@ public final class C {
ENCODING_PCM_A_LAW,
ENCODING_AC3,
ENCODING_E_AC3,
+ ENCODING_E_AC3_JOC,
+ ENCODING_AC4,
ENCODING_DTS,
ENCODING_DTS_HD,
- ENCODING_DOLBY_TRUEHD
+ ENCODING_DOLBY_TRUEHD,
})
public @interface Encoding {}
@@ -169,6 +178,7 @@ public final class C {
* #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT}, {@link #ENCODING_PCM_FLOAT}, {@link
* #ENCODING_PCM_MU_LAW} or {@link #ENCODING_PCM_A_LAW}.
*/
+ @Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({
Format.NO_VALUE,
@@ -202,6 +212,10 @@ public final class C {
public static final int ENCODING_AC3 = AudioFormat.ENCODING_AC3;
/** @see AudioFormat#ENCODING_E_AC3 */
public static final int ENCODING_E_AC3 = AudioFormat.ENCODING_E_AC3;
+ /** @see AudioFormat#ENCODING_E_AC3_JOC */
+ public static final int ENCODING_E_AC3_JOC = AudioFormat.ENCODING_E_AC3_JOC;
+ /** @see AudioFormat#ENCODING_AC4 */
+ public static final int ENCODING_AC4 = AudioFormat.ENCODING_AC4;
/** @see AudioFormat#ENCODING_DTS */
public static final int ENCODING_DTS = AudioFormat.ENCODING_DTS;
/** @see AudioFormat#ENCODING_DTS_HD */
@@ -215,6 +229,7 @@ public final class C {
* #STREAM_TYPE_RING}, {@link #STREAM_TYPE_SYSTEM}, {@link #STREAM_TYPE_VOICE_CALL} or {@link
* #STREAM_TYPE_USE_DEFAULT}.
*/
+ @Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({
STREAM_TYPE_ALARM,
@@ -269,6 +284,7 @@ public final class C {
* #CONTENT_TYPE_MOVIE}, {@link #CONTENT_TYPE_MUSIC}, {@link #CONTENT_TYPE_SONIFICATION}, {@link
* #CONTENT_TYPE_SPEECH} or {@link #CONTENT_TYPE_UNKNOWN}.
*/
+ @Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({
CONTENT_TYPE_MOVIE,
@@ -309,6 +325,7 @@ public final class C {
*
Note that {@code FLAG_HW_AV_SYNC} is not available because the player takes care of setting
* the flag when tunneling is enabled via a track selector.
*/
+ @Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef(
flag = true,
@@ -331,6 +348,7 @@ public final class C {
* #USAGE_UNKNOWN}, {@link #USAGE_VOICE_COMMUNICATION} or {@link
* #USAGE_VOICE_COMMUNICATION_SIGNALLING}.
*/
+ @Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({
USAGE_ALARM,
@@ -427,6 +445,7 @@ public final class C {
* #AUDIOFOCUS_GAIN_TRANSIENT}, {@link #AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK} or {@link
* #AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE}.
*/
+ @Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({
AUDIOFOCUS_NONE,
@@ -451,15 +470,17 @@ public final class C {
/**
* Flags which can apply to a buffer containing a media sample. Possible flag values are {@link
- * #BUFFER_FLAG_KEY_FRAME}, {@link #BUFFER_FLAG_END_OF_STREAM}, {@link #BUFFER_FLAG_ENCRYPTED} and
- * {@link #BUFFER_FLAG_DECODE_ONLY}.
+ * #BUFFER_FLAG_KEY_FRAME}, {@link #BUFFER_FLAG_END_OF_STREAM}, {@link #BUFFER_FLAG_LAST_SAMPLE},
+ * {@link #BUFFER_FLAG_ENCRYPTED} and {@link #BUFFER_FLAG_DECODE_ONLY}.
*/
+ @Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef(
flag = true,
value = {
BUFFER_FLAG_KEY_FRAME,
BUFFER_FLAG_END_OF_STREAM,
+ BUFFER_FLAG_LAST_SAMPLE,
BUFFER_FLAG_ENCRYPTED,
BUFFER_FLAG_DECODE_ONLY
})
@@ -472,16 +493,33 @@ public final class C {
* Flag for empty buffers that signal that the end of the stream was reached.
*/
public static final int BUFFER_FLAG_END_OF_STREAM = MediaCodec.BUFFER_FLAG_END_OF_STREAM;
+ /** Indicates that a buffer is known to contain the last media sample of the stream. */
+ public static final int BUFFER_FLAG_LAST_SAMPLE = 1 << 29; // 0x20000000
/** Indicates that a buffer is (at least partially) encrypted. */
public static final int BUFFER_FLAG_ENCRYPTED = 1 << 30; // 0x40000000
/** Indicates that a buffer should be decoded but not rendered. */
- @SuppressWarnings("NumericOverflow")
public static final int BUFFER_FLAG_DECODE_ONLY = 1 << 31; // 0x80000000
+ /**
+ * Video decoder output modes. Possible modes are {@link #VIDEO_OUTPUT_MODE_NONE}, {@link
+ * #VIDEO_OUTPUT_MODE_YUV} and {@link #VIDEO_OUTPUT_MODE_SURFACE_YUV}.
+ */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {VIDEO_OUTPUT_MODE_NONE, VIDEO_OUTPUT_MODE_YUV, VIDEO_OUTPUT_MODE_SURFACE_YUV})
+ public @interface VideoOutputMode {}
+ /** Video decoder output mode is not set. */
+ public static final int VIDEO_OUTPUT_MODE_NONE = -1;
+ /** Video decoder output mode that outputs raw 4:2:0 YUV planes. */
+ public static final int VIDEO_OUTPUT_MODE_YUV = 0;
+ /** Video decoder output mode that renders 4:2:0 YUV planes directly to a surface. */
+ public static final int VIDEO_OUTPUT_MODE_SURFACE_YUV = 1;
+
/**
* Video scaling modes for {@link MediaCodec}-based {@link Renderer}s. One of {@link
* #VIDEO_SCALING_MODE_SCALE_TO_FIT} or {@link #VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING}.
*/
+ @Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef(value = {VIDEO_SCALING_MODE_SCALE_TO_FIT, VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING})
public @interface VideoScalingMode {}
@@ -504,6 +542,7 @@ public final class C {
* Track selection flags. Possible flag values are {@link #SELECTION_FLAG_DEFAULT}, {@link
* #SELECTION_FLAG_FORCED} and {@link #SELECTION_FLAG_AUTOSELECT}.
*/
+ @Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef(
flag = true,
@@ -521,15 +560,14 @@ public final class C {
*/
public static final int SELECTION_FLAG_AUTOSELECT = 1 << 2; // 4
- /**
- * Represents an undetermined language as an ISO 639 alpha-3 language code.
- */
+ /** Represents an undetermined language as an ISO 639-2 language code. */
public static final String LANGUAGE_UNDETERMINED = "und";
/**
* Represents a streaming or other media type. One of {@link #TYPE_DASH}, {@link #TYPE_SS}, {@link
* #TYPE_HLS} or {@link #TYPE_OTHER}.
*/
+ @Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({TYPE_DASH, TYPE_SS, TYPE_HLS, TYPE_OTHER})
public @interface ContentType {}
@@ -796,6 +834,7 @@ public final class C {
* #STEREO_MODE_MONO}, {@link #STEREO_MODE_TOP_BOTTOM}, {@link #STEREO_MODE_LEFT_RIGHT} or {@link
* #STEREO_MODE_STEREO_MESH}.
*/
+ @Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({
Format.NO_VALUE,
@@ -827,6 +866,7 @@ public final class C {
* Video colorspaces. One of {@link Format#NO_VALUE}, {@link #COLOR_SPACE_BT709}, {@link
* #COLOR_SPACE_BT601} or {@link #COLOR_SPACE_BT2020}.
*/
+ @Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({Format.NO_VALUE, COLOR_SPACE_BT709, COLOR_SPACE_BT601, COLOR_SPACE_BT2020})
public @interface ColorSpace {}
@@ -847,6 +887,7 @@ public final class C {
* Video color transfer characteristics. One of {@link Format#NO_VALUE}, {@link
* #COLOR_TRANSFER_SDR}, {@link #COLOR_TRANSFER_ST2084} or {@link #COLOR_TRANSFER_HLG}.
*/
+ @Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({Format.NO_VALUE, COLOR_TRANSFER_SDR, COLOR_TRANSFER_ST2084, COLOR_TRANSFER_HLG})
public @interface ColorTransfer {}
@@ -867,6 +908,7 @@ public final class C {
* Video color range. One of {@link Format#NO_VALUE}, {@link #COLOR_RANGE_LIMITED} or {@link
* #COLOR_RANGE_FULL}.
*/
+ @Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({Format.NO_VALUE, COLOR_RANGE_LIMITED, COLOR_RANGE_FULL})
public @interface ColorRange {}
@@ -879,6 +921,26 @@ public final class C {
*/
public static final int COLOR_RANGE_FULL = MediaFormat.COLOR_RANGE_FULL;
+ /** Video projection types. */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ Format.NO_VALUE,
+ PROJECTION_RECTANGULAR,
+ PROJECTION_EQUIRECTANGULAR,
+ PROJECTION_CUBEMAP,
+ PROJECTION_MESH
+ })
+ public @interface Projection {}
+ /** Conventional rectangular projection. */
+ public static final int PROJECTION_RECTANGULAR = 0;
+ /** Equirectangular spherical projection. */
+ public static final int PROJECTION_EQUIRECTANGULAR = 1;
+ /** Cube map projection. */
+ public static final int PROJECTION_CUBEMAP = 2;
+ /** 3-D mesh projection. */
+ public static final int PROJECTION_MESH = 3;
+
/**
* Priority for media playback.
*
@@ -899,6 +961,7 @@ public final class C {
* #NETWORK_TYPE_4G}, {@link #NETWORK_TYPE_CELLULAR_UNKNOWN}, {@link #NETWORK_TYPE_ETHERNET} or
* {@link #NETWORK_TYPE_OTHER}.
*/
+ @Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({
NETWORK_TYPE_UNKNOWN,
@@ -937,6 +1000,79 @@ public final class C {
*/
public static final int NETWORK_TYPE_OTHER = 8;
+ /**
+ * Track role flags. Possible flag values are {@link #ROLE_FLAG_MAIN}, {@link
+ * #ROLE_FLAG_ALTERNATE}, {@link #ROLE_FLAG_SUPPLEMENTARY}, {@link #ROLE_FLAG_COMMENTARY}, {@link
+ * #ROLE_FLAG_DUB}, {@link #ROLE_FLAG_EMERGENCY}, {@link #ROLE_FLAG_CAPTION}, {@link
+ * #ROLE_FLAG_SUBTITLE}, {@link #ROLE_FLAG_SIGN}, {@link #ROLE_FLAG_DESCRIBES_VIDEO}, {@link
+ * #ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND}, {@link #ROLE_FLAG_ENHANCED_DIALOG_INTELLIGIBILITY},
+ * {@link #ROLE_FLAG_TRANSCRIBES_DIALOG} and {@link #ROLE_FLAG_EASY_TO_READ}.
+ */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ flag = true,
+ value = {
+ ROLE_FLAG_MAIN,
+ ROLE_FLAG_ALTERNATE,
+ ROLE_FLAG_SUPPLEMENTARY,
+ ROLE_FLAG_COMMENTARY,
+ ROLE_FLAG_DUB,
+ ROLE_FLAG_EMERGENCY,
+ ROLE_FLAG_CAPTION,
+ ROLE_FLAG_SUBTITLE,
+ ROLE_FLAG_SIGN,
+ ROLE_FLAG_DESCRIBES_VIDEO,
+ ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND,
+ ROLE_FLAG_ENHANCED_DIALOG_INTELLIGIBILITY,
+ ROLE_FLAG_TRANSCRIBES_DIALOG,
+ ROLE_FLAG_EASY_TO_READ
+ })
+ public @interface RoleFlags {}
+ /** Indicates a main track. */
+ public static final int ROLE_FLAG_MAIN = 1;
+ /**
+ * Indicates an alternate track. For example a video track recorded from an different view point
+ * than the main track(s).
+ */
+ public static final int ROLE_FLAG_ALTERNATE = 1 << 1;
+ /**
+ * Indicates a supplementary track, meaning the track has lower importance than the main track(s).
+ * For example a video track that provides a visual accompaniment to a main audio track.
+ */
+ public static final int ROLE_FLAG_SUPPLEMENTARY = 1 << 2;
+ /** Indicates the track contains commentary, for example from the director. */
+ public static final int ROLE_FLAG_COMMENTARY = 1 << 3;
+ /**
+ * Indicates the track is in a different language from the original, for example dubbed audio or
+ * translated captions.
+ */
+ public static final int ROLE_FLAG_DUB = 1 << 4;
+ /** Indicates the track contains information about a current emergency. */
+ public static final int ROLE_FLAG_EMERGENCY = 1 << 5;
+ /**
+ * Indicates the track contains captions. This flag may be set on video tracks to indicate the
+ * presence of burned in captions.
+ */
+ public static final int ROLE_FLAG_CAPTION = 1 << 6;
+ /**
+ * Indicates the track contains subtitles. This flag may be set on video tracks to indicate the
+ * presence of burned in subtitles.
+ */
+ public static final int ROLE_FLAG_SUBTITLE = 1 << 7;
+ /** Indicates the track contains a visual sign-language interpretation of an audio track. */
+ public static final int ROLE_FLAG_SIGN = 1 << 8;
+ /** Indicates the track contains an audio or textual description of a video track. */
+ public static final int ROLE_FLAG_DESCRIBES_VIDEO = 1 << 9;
+ /** Indicates the track contains a textual description of music and sound. */
+ public static final int ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND = 1 << 10;
+ /** Indicates the track is designed for improved intelligibility of dialogue. */
+ public static final int ROLE_FLAG_ENHANCED_DIALOG_INTELLIGIBILITY = 1 << 11;
+ /** Indicates the track contains a transcription of spoken dialog. */
+ public static final int ROLE_FLAG_TRANSCRIBES_DIALOG = 1 << 12;
+ /** Indicates the track contains a text that has been edited for ease of reading. */
+ public static final int ROLE_FLAG_EASY_TO_READ = 1 << 13;
+
/**
* Converts a time in microseconds to the corresponding time in milliseconds, preserving
* {@link #TIME_UNSET} and {@link #TIME_END_OF_SOURCE} values.
@@ -960,7 +1096,10 @@ public final class C {
}
/**
- * Returns a newly generated {@link android.media.AudioTrack} session identifier.
+ * Returns a newly generated audio session identifier, or {@link AudioManager#ERROR} if an error
+ * occurred in which case audio playback may fail.
+ *
+ * @see AudioManager#generateAudioSessionId()
*/
@TargetApi(21)
public static int generateAudioSessionIdV21(Context context) {
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 c466815c79..972f651a41 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
@@ -20,7 +20,6 @@ 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;
/**
@@ -30,12 +29,14 @@ public class DefaultLoadControl implements LoadControl {
/**
* The default minimum duration of media that the player will attempt to ensure is buffered at all
- * times, in milliseconds.
+ * times, in milliseconds. This value is only applied to playbacks without video.
*/
public static final int DEFAULT_MIN_BUFFER_MS = 15000;
/**
* The default maximum duration of media that the player will attempt to buffer, in milliseconds.
+ * For playbacks with video, this is also the default minimum duration of media that the player
+ * will attempt to ensure is buffered.
*/
public static final int DEFAULT_MAX_BUFFER_MS = 50000;
@@ -52,36 +53,46 @@ public class DefaultLoadControl implements LoadControl {
public static final int DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 5000;
/**
- * The default target buffer size in bytes. When set to {@link C#LENGTH_UNSET}, the load control
- * automatically determines its target buffer size.
+ * The default target buffer size in bytes. The value ({@link C#LENGTH_UNSET}) means that the load
+ * control will calculate the target buffer size based on the selected tracks.
*/
public static final int DEFAULT_TARGET_BUFFER_BYTES = C.LENGTH_UNSET;
/** The default prioritization of buffer time constraints over size constraints. */
public static final boolean DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS = true;
+ /** The default back buffer duration in milliseconds. */
+ public static final int DEFAULT_BACK_BUFFER_DURATION_MS = 0;
+
+ /** The default for whether the back buffer is retained from the previous keyframe. */
+ public static final boolean DEFAULT_RETAIN_BACK_BUFFER_FROM_KEYFRAME = false;
+
/** Builder for {@link DefaultLoadControl}. */
public static final class Builder {
private DefaultAllocator allocator;
- private int minBufferMs;
+ private int minBufferAudioMs;
+ private int minBufferVideoMs;
private int maxBufferMs;
private int bufferForPlaybackMs;
private int bufferForPlaybackAfterRebufferMs;
private int targetBufferBytes;
private boolean prioritizeTimeOverSizeThresholds;
- private PriorityTaskManager priorityTaskManager;
+ private int backBufferDurationMs;
+ private boolean retainBackBufferFromKeyframe;
+ private boolean createDefaultLoadControlCalled;
/** Constructs a new instance. */
public Builder() {
- allocator = null;
- minBufferMs = DEFAULT_MIN_BUFFER_MS;
+ minBufferAudioMs = DEFAULT_MIN_BUFFER_MS;
+ minBufferVideoMs = DEFAULT_MAX_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;
+ backBufferDurationMs = DEFAULT_BACK_BUFFER_DURATION_MS;
+ retainBackBufferFromKeyframe = DEFAULT_RETAIN_BACK_BUFFER_FROM_KEYFRAME;
}
/**
@@ -89,8 +100,10 @@ public class DefaultLoadControl implements LoadControl {
*
* @param allocator The {@link DefaultAllocator}.
* @return This builder, for convenience.
+ * @throws IllegalStateException If {@link #createDefaultLoadControl()} has already been called.
*/
public Builder setAllocator(DefaultAllocator allocator) {
+ Assertions.checkState(!createDefaultLoadControlCalled);
this.allocator = allocator;
return this;
}
@@ -108,13 +121,26 @@ public class DefaultLoadControl implements LoadControl {
* 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.
+ * @throws IllegalStateException If {@link #createDefaultLoadControl()} has already been called.
*/
public Builder setBufferDurationsMs(
int minBufferMs,
int maxBufferMs,
int bufferForPlaybackMs,
int bufferForPlaybackAfterRebufferMs) {
- this.minBufferMs = minBufferMs;
+ Assertions.checkState(!createDefaultLoadControlCalled);
+ 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.minBufferAudioMs = minBufferMs;
+ this.minBufferVideoMs = minBufferMs;
this.maxBufferMs = maxBufferMs;
this.bufferForPlaybackMs = bufferForPlaybackMs;
this.bufferForPlaybackAfterRebufferMs = bufferForPlaybackAfterRebufferMs;
@@ -123,13 +149,14 @@ public class DefaultLoadControl implements LoadControl {
/**
* 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)}.
+ * size will be calculated based on the selected tracks.
*
* @param targetBufferBytes The target buffer size in bytes.
* @return This builder, for convenience.
+ * @throws IllegalStateException If {@link #createDefaultLoadControl()} has already been called.
*/
public Builder setTargetBufferBytes(int targetBufferBytes) {
+ Assertions.checkState(!createDefaultLoadControlCalled);
this.targetBufferBytes = targetBufferBytes;
return this;
}
@@ -141,48 +168,68 @@ public class DefaultLoadControl implements LoadControl {
* @param prioritizeTimeOverSizeThresholds Whether the load control prioritizes buffer time
* constraints over buffer size constraints.
* @return This builder, for convenience.
+ * @throws IllegalStateException If {@link #createDefaultLoadControl()} has already been called.
*/
public Builder setPrioritizeTimeOverSizeThresholds(boolean prioritizeTimeOverSizeThresholds) {
+ Assertions.checkState(!createDefaultLoadControlCalled);
this.prioritizeTimeOverSizeThresholds = prioritizeTimeOverSizeThresholds;
return this;
}
- /** Sets the {@link PriorityTaskManager} to use. */
- public Builder setPriorityTaskManager(PriorityTaskManager priorityTaskManager) {
- this.priorityTaskManager = priorityTaskManager;
+ /**
+ * Sets the back buffer duration, and whether the back buffer is retained from the previous
+ * keyframe.
+ *
+ * @param backBufferDurationMs The back buffer duration in milliseconds.
+ * @param retainBackBufferFromKeyframe Whether the back buffer is retained from the previous
+ * keyframe.
+ * @return This builder, for convenience.
+ * @throws IllegalStateException If {@link #createDefaultLoadControl()} has already been called.
+ */
+ public Builder setBackBuffer(int backBufferDurationMs, boolean retainBackBufferFromKeyframe) {
+ Assertions.checkState(!createDefaultLoadControlCalled);
+ assertGreaterOrEqual(backBufferDurationMs, 0, "backBufferDurationMs", "0");
+ this.backBufferDurationMs = backBufferDurationMs;
+ this.retainBackBufferFromKeyframe = retainBackBufferFromKeyframe;
return this;
}
/** Creates a {@link DefaultLoadControl}. */
- @SuppressWarnings("deprecation")
public DefaultLoadControl createDefaultLoadControl() {
+ Assertions.checkState(!createDefaultLoadControlCalled);
+ createDefaultLoadControlCalled = true;
if (allocator == null) {
- allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE);
+ allocator = new DefaultAllocator(/* trimOnReset= */ true, C.DEFAULT_BUFFER_SEGMENT_SIZE);
}
return new DefaultLoadControl(
allocator,
- minBufferMs,
+ minBufferAudioMs,
+ minBufferVideoMs,
maxBufferMs,
bufferForPlaybackMs,
bufferForPlaybackAfterRebufferMs,
targetBufferBytes,
prioritizeTimeOverSizeThresholds,
- priorityTaskManager);
+ backBufferDurationMs,
+ retainBackBufferFromKeyframe);
}
}
private final DefaultAllocator allocator;
- private final long minBufferUs;
+ private final long minBufferAudioUs;
+ private final long minBufferVideoUs;
private final long maxBufferUs;
private final long bufferForPlaybackUs;
private final long bufferForPlaybackAfterRebufferUs;
private final int targetBufferBytesOverwrite;
private final boolean prioritizeTimeOverSizeThresholds;
- private final PriorityTaskManager priorityTaskManager;
+ private final long backBufferDurationUs;
+ private final boolean retainBackBufferFromKeyframe;
private int targetBufferSize;
private boolean isBuffering;
+ private boolean hasVideo;
/** Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class. */
@SuppressWarnings("deprecation")
@@ -192,21 +239,22 @@ public class DefaultLoadControl implements LoadControl {
/** @deprecated Use {@link Builder} instead. */
@Deprecated
- @SuppressWarnings("deprecation")
public DefaultLoadControl(DefaultAllocator allocator) {
this(
allocator,
- DEFAULT_MIN_BUFFER_MS,
+ /* minBufferAudioMs= */ DEFAULT_MIN_BUFFER_MS,
+ /* minBufferVideoMs= */ DEFAULT_MAX_BUFFER_MS,
DEFAULT_MAX_BUFFER_MS,
DEFAULT_BUFFER_FOR_PLAYBACK_MS,
DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS,
DEFAULT_TARGET_BUFFER_BYTES,
- DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS);
+ DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS,
+ DEFAULT_BACK_BUFFER_DURATION_MS,
+ DEFAULT_RETAIN_BACK_BUFFER_FROM_KEYFRAME);
}
/** @deprecated Use {@link Builder} instead. */
@Deprecated
- @SuppressWarnings("deprecation")
public DefaultLoadControl(
DefaultAllocator allocator,
int minBufferMs,
@@ -217,45 +265,59 @@ public class DefaultLoadControl implements LoadControl {
boolean prioritizeTimeOverSizeThresholds) {
this(
allocator,
- minBufferMs,
+ /* minBufferAudioMs= */ minBufferMs,
+ /* minBufferVideoMs= */ minBufferMs,
maxBufferMs,
bufferForPlaybackMs,
bufferForPlaybackAfterRebufferMs,
targetBufferBytes,
prioritizeTimeOverSizeThresholds,
- null);
+ DEFAULT_BACK_BUFFER_DURATION_MS,
+ DEFAULT_RETAIN_BACK_BUFFER_FROM_KEYFRAME);
}
- /** @deprecated Use {@link Builder} instead. */
- @Deprecated
- public DefaultLoadControl(
+ protected DefaultLoadControl(
DefaultAllocator allocator,
- int minBufferMs,
+ int minBufferAudioMs,
+ int minBufferVideoMs,
int maxBufferMs,
int bufferForPlaybackMs,
int bufferForPlaybackAfterRebufferMs,
int targetBufferBytes,
boolean prioritizeTimeOverSizeThresholds,
- PriorityTaskManager priorityTaskManager) {
+ int backBufferDurationMs,
+ boolean retainBackBufferFromKeyframe) {
assertGreaterOrEqual(bufferForPlaybackMs, 0, "bufferForPlaybackMs", "0");
assertGreaterOrEqual(
bufferForPlaybackAfterRebufferMs, 0, "bufferForPlaybackAfterRebufferMs", "0");
- assertGreaterOrEqual(minBufferMs, bufferForPlaybackMs, "minBufferMs", "bufferForPlaybackMs");
assertGreaterOrEqual(
- minBufferMs,
+ minBufferAudioMs, bufferForPlaybackMs, "minBufferAudioMs", "bufferForPlaybackMs");
+ assertGreaterOrEqual(
+ minBufferVideoMs, bufferForPlaybackMs, "minBufferVideoMs", "bufferForPlaybackMs");
+ assertGreaterOrEqual(
+ minBufferAudioMs,
bufferForPlaybackAfterRebufferMs,
- "minBufferMs",
+ "minBufferAudioMs",
"bufferForPlaybackAfterRebufferMs");
- assertGreaterOrEqual(maxBufferMs, minBufferMs, "maxBufferMs", "minBufferMs");
+ assertGreaterOrEqual(
+ minBufferVideoMs,
+ bufferForPlaybackAfterRebufferMs,
+ "minBufferVideoMs",
+ "bufferForPlaybackAfterRebufferMs");
+ assertGreaterOrEqual(maxBufferMs, minBufferAudioMs, "maxBufferMs", "minBufferAudioMs");
+ assertGreaterOrEqual(maxBufferMs, minBufferVideoMs, "maxBufferMs", "minBufferVideoMs");
+ assertGreaterOrEqual(backBufferDurationMs, 0, "backBufferDurationMs", "0");
this.allocator = allocator;
- minBufferUs = minBufferMs * 1000L;
- maxBufferUs = maxBufferMs * 1000L;
- bufferForPlaybackUs = bufferForPlaybackMs * 1000L;
- bufferForPlaybackAfterRebufferUs = bufferForPlaybackAfterRebufferMs * 1000L;
- targetBufferBytesOverwrite = targetBufferBytes;
+ this.minBufferAudioUs = C.msToUs(minBufferAudioMs);
+ this.minBufferVideoUs = C.msToUs(minBufferVideoMs);
+ this.maxBufferUs = C.msToUs(maxBufferMs);
+ this.bufferForPlaybackUs = C.msToUs(bufferForPlaybackMs);
+ this.bufferForPlaybackAfterRebufferUs = C.msToUs(bufferForPlaybackAfterRebufferMs);
+ this.targetBufferBytesOverwrite = targetBufferBytes;
this.prioritizeTimeOverSizeThresholds = prioritizeTimeOverSizeThresholds;
- this.priorityTaskManager = priorityTaskManager;
+ this.backBufferDurationUs = C.msToUs(backBufferDurationMs);
+ this.retainBackBufferFromKeyframe = retainBackBufferFromKeyframe;
}
@Override
@@ -266,6 +328,7 @@ public class DefaultLoadControl implements LoadControl {
@Override
public void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroups,
TrackSelectionArray trackSelections) {
+ hasVideo = hasVideo(renderers, trackSelections);
targetBufferSize =
targetBufferBytesOverwrite == C.LENGTH_UNSET
? calculateTargetBufferSize(renderers, trackSelections)
@@ -290,19 +353,18 @@ public class DefaultLoadControl implements LoadControl {
@Override
public long getBackBufferDurationUs() {
- return 0;
+ return backBufferDurationUs;
}
@Override
public boolean retainBackBufferFromKeyframe() {
- return false;
+ return retainBackBufferFromKeyframe;
}
@Override
public boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed) {
boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize;
- boolean wasBuffering = isBuffering;
- long minBufferUs = this.minBufferUs;
+ long minBufferUs = hasVideo ? minBufferVideoUs : minBufferAudioUs;
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.
@@ -312,16 +374,9 @@ public class DefaultLoadControl implements LoadControl {
}
if (bufferedDurationUs < minBufferUs) {
isBuffering = prioritizeTimeOverSizeThresholds || !targetBufferSizeReached;
- } else if (bufferedDurationUs > maxBufferUs || 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);
- } else {
- priorityTaskManager.remove(C.PRIORITY_PLAYBACK);
- }
- }
return isBuffering;
}
@@ -357,15 +412,21 @@ public class DefaultLoadControl implements LoadControl {
private void reset(boolean resetAllocator) {
targetBufferSize = 0;
- if (priorityTaskManager != null && isBuffering) {
- priorityTaskManager.remove(C.PRIORITY_PLAYBACK);
- }
isBuffering = false;
if (resetAllocator) {
allocator.reset();
}
}
+ private static boolean hasVideo(Renderer[] renderers, TrackSelectionArray trackSelectionArray) {
+ for (int i = 0; i < renderers.length; i++) {
+ if (renderers[i].getTrackType() == C.TRACK_TYPE_VIDEO && trackSelectionArray.get(i) != null) {
+ return true;
+ }
+ }
+ return false;
+ }
+
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/DefaultMediaClock.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java
index ed57cec70c..1971a4cefc 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java
@@ -15,7 +15,7 @@
*/
package com.google.android.exoplayer2;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.MediaClock;
import com.google.android.exoplayer2.util.StandaloneMediaClock;
@@ -32,19 +32,21 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock;
public interface PlaybackParameterListener {
/**
- * Called when the active playback parameters changed.
+ * Called when the active playback parameters changed. Will not be called for {@link
+ * #setPlaybackParameters(PlaybackParameters)}.
*
* @param newPlaybackParameters The newly active {@link PlaybackParameters}.
*/
void onPlaybackParametersChanged(PlaybackParameters newPlaybackParameters);
-
}
- private final StandaloneMediaClock standaloneMediaClock;
+ private final StandaloneMediaClock standaloneClock;
private final PlaybackParameterListener listener;
- private @Nullable Renderer rendererClockSource;
- private @Nullable MediaClock rendererClock;
+ @Nullable private Renderer rendererClockSource;
+ @Nullable private MediaClock rendererClock;
+ private boolean isUsingStandaloneClock;
+ private boolean standaloneClockIsStarted;
/**
* Creates a new instance with listener for playback parameter changes and a {@link Clock} to use
@@ -56,21 +58,24 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock;
*/
public DefaultMediaClock(PlaybackParameterListener listener, Clock clock) {
this.listener = listener;
- this.standaloneMediaClock = new StandaloneMediaClock(clock);
+ this.standaloneClock = new StandaloneMediaClock(clock);
+ isUsingStandaloneClock = true;
}
/**
* Starts the standalone fallback clock.
*/
public void start() {
- standaloneMediaClock.start();
+ standaloneClockIsStarted = true;
+ standaloneClock.start();
}
/**
* Stops the standalone fallback clock.
*/
public void stop() {
- standaloneMediaClock.stop();
+ standaloneClockIsStarted = false;
+ standaloneClock.stop();
}
/**
@@ -79,7 +84,7 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock;
* @param positionUs The position to set in microseconds.
*/
public void resetPosition(long positionUs) {
- standaloneMediaClock.resetPosition(positionUs);
+ standaloneClock.resetPosition(positionUs);
}
/**
@@ -99,8 +104,7 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock;
}
this.rendererClock = rendererMediaClock;
this.rendererClockSource = renderer;
- rendererClock.setPlaybackParameters(standaloneMediaClock.getPlaybackParameters());
- ensureSynced();
+ rendererClock.setPlaybackParameters(standaloneClock.getPlaybackParameters());
}
}
@@ -114,65 +118,80 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock;
if (renderer == rendererClockSource) {
this.rendererClock = null;
this.rendererClockSource = null;
+ isUsingStandaloneClock = true;
}
}
/**
* Syncs internal clock if needed and returns current clock position in microseconds.
+ *
+ * @param isReadingAhead Whether the renderers are reading ahead.
*/
- public long syncAndGetPositionUs() {
- if (isUsingRendererClock()) {
- ensureSynced();
- return rendererClock.getPositionUs();
- } else {
- return standaloneMediaClock.getPositionUs();
- }
+ public long syncAndGetPositionUs(boolean isReadingAhead) {
+ syncClocks(isReadingAhead);
+ return getPositionUs();
}
// MediaClock implementation.
@Override
public long getPositionUs() {
- if (isUsingRendererClock()) {
- return rendererClock.getPositionUs();
- } else {
- return standaloneMediaClock.getPositionUs();
- }
+ return isUsingStandaloneClock ? standaloneClock.getPositionUs() : rendererClock.getPositionUs();
}
@Override
- public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) {
+ public void setPlaybackParameters(PlaybackParameters playbackParameters) {
if (rendererClock != null) {
- playbackParameters = rendererClock.setPlaybackParameters(playbackParameters);
+ rendererClock.setPlaybackParameters(playbackParameters);
+ playbackParameters = rendererClock.getPlaybackParameters();
}
- standaloneMediaClock.setPlaybackParameters(playbackParameters);
- listener.onPlaybackParametersChanged(playbackParameters);
- return playbackParameters;
+ standaloneClock.setPlaybackParameters(playbackParameters);
}
@Override
public PlaybackParameters getPlaybackParameters() {
- return rendererClock != null ? rendererClock.getPlaybackParameters()
- : standaloneMediaClock.getPlaybackParameters();
+ return rendererClock != null
+ ? rendererClock.getPlaybackParameters()
+ : standaloneClock.getPlaybackParameters();
}
- private void ensureSynced() {
+ private void syncClocks(boolean isReadingAhead) {
+ if (shouldUseStandaloneClock(isReadingAhead)) {
+ isUsingStandaloneClock = true;
+ if (standaloneClockIsStarted) {
+ standaloneClock.start();
+ }
+ return;
+ }
long rendererClockPositionUs = rendererClock.getPositionUs();
- standaloneMediaClock.resetPosition(rendererClockPositionUs);
+ if (isUsingStandaloneClock) {
+ // Ensure enabling the renderer clock doesn't jump backwards in time.
+ if (rendererClockPositionUs < standaloneClock.getPositionUs()) {
+ standaloneClock.stop();
+ return;
+ }
+ isUsingStandaloneClock = false;
+ if (standaloneClockIsStarted) {
+ standaloneClock.start();
+ }
+ }
+ // Continuously sync stand-alone clock to renderer clock so that it can take over if needed.
+ standaloneClock.resetPosition(rendererClockPositionUs);
PlaybackParameters playbackParameters = rendererClock.getPlaybackParameters();
- if (!playbackParameters.equals(standaloneMediaClock.getPlaybackParameters())) {
- standaloneMediaClock.setPlaybackParameters(playbackParameters);
+ if (!playbackParameters.equals(standaloneClock.getPlaybackParameters())) {
+ standaloneClock.setPlaybackParameters(playbackParameters);
listener.onPlaybackParametersChanged(playbackParameters);
}
}
- private boolean isUsingRendererClock() {
- // Use the renderer clock if the providing renderer has not ended or needs the next sample
- // stream to reenter the ready state. The latter case uses the standalone clock to avoid getting
- // stuck if tracks in the current period have uneven durations.
- // See: https://github.com/google/ExoPlayer/issues/1874.
- return rendererClockSource != null && !rendererClockSource.isEnded()
- && (rendererClockSource.isReady() || !rendererClockSource.hasReadStreamToEnd());
+ private boolean shouldUseStandaloneClock(boolean isReadingAhead) {
+ // Use the standalone clock if the clock providing renderer is not set or has ended. Also use
+ // the standalone clock if the renderer is not ready and we have finished reading the stream or
+ // are reading ahead to avoid getting stuck if tracks in the current period have uneven
+ // durations. See: https://github.com/google/ExoPlayer/issues/1874.
+ return rendererClockSource == null
+ || rendererClockSource.isEnded()
+ || (!rendererClockSource.isReady()
+ && (isReadingAhead || rendererClockSource.hasReadStreamToEnd()));
}
-
}
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 c0a117c241..490d961396 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
@@ -16,14 +16,15 @@
package com.google.android.exoplayer2;
import android.content.Context;
+import android.media.MediaCodec;
import android.os.Handler;
import android.os.Looper;
-import android.support.annotation.IntDef;
-import android.support.annotation.Nullable;
-import android.util.Log;
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.audio.AudioCapabilities;
import com.google.android.exoplayer2.audio.AudioProcessor;
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
+import com.google.android.exoplayer2.audio.DefaultAudioSink;
import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
@@ -33,9 +34,11 @@ import com.google.android.exoplayer2.metadata.MetadataRenderer;
import com.google.android.exoplayer2.text.TextOutput;
import com.google.android.exoplayer2.text.TextRenderer;
import com.google.android.exoplayer2.trackselection.TrackSelector;
+import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
import com.google.android.exoplayer2.video.VideoRendererEventListener;
import com.google.android.exoplayer2.video.spherical.CameraMotionRenderer;
+import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Constructor;
@@ -56,6 +59,7 @@ public class DefaultRenderersFactory implements RenderersFactory {
* Modes for using extension renderers. One of {@link #EXTENSION_RENDERER_MODE_OFF}, {@link
* #EXTENSION_RENDERER_MODE_ON} or {@link #EXTENSION_RENDERER_MODE_PREFER}.
*/
+ @Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({EXTENSION_RENDERER_MODE_OFF, EXTENSION_RENDERER_MODE_ON, EXTENSION_RENDERER_MODE_PREFER})
public @interface ExtensionRendererMode {}
@@ -83,15 +87,19 @@ public class DefaultRenderersFactory implements RenderersFactory {
protected static final int MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY = 50;
private final Context context;
- private final @Nullable DrmSessionManager drmSessionManager;
- private final @ExtensionRendererMode int extensionRendererMode;
- private final long allowedVideoJoiningTimeMs;
+ @Nullable private DrmSessionManager drmSessionManager;
+ @ExtensionRendererMode private int extensionRendererMode;
+ private long allowedVideoJoiningTimeMs;
+ private boolean playClearSamplesWithoutKeys;
+ private boolean enableDecoderFallback;
+ private MediaCodecSelector mediaCodecSelector;
- /**
- * @param context A {@link Context}.
- */
+ /** @param context A {@link Context}. */
public DefaultRenderersFactory(Context context) {
- this(context, EXTENSION_RENDERER_MODE_OFF);
+ this.context = context;
+ extensionRendererMode = EXTENSION_RENDERER_MODE_OFF;
+ allowedVideoJoiningTimeMs = DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS;
+ mediaCodecSelector = MediaCodecSelector.DEFAULT;
}
/**
@@ -106,19 +114,20 @@ public class DefaultRenderersFactory implements RenderersFactory {
}
/**
- * @param context A {@link Context}.
- * @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.
+ * @deprecated Use {@link #DefaultRenderersFactory(Context)} and {@link
+ * #setExtensionRendererMode(int)}.
*/
+ @Deprecated
+ @SuppressWarnings("deprecation")
public DefaultRenderersFactory(
Context context, @ExtensionRendererMode int extensionRendererMode) {
this(context, 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 Use {@link #DefaultRenderersFactory(Context)} and {@link
+ * #setExtensionRendererMode(int)}, and pass {@link DrmSessionManager} directly to {@link
+ * SimpleExoPlayer} or {@link ExoPlayerFactory}.
*/
@Deprecated
@SuppressWarnings("deprecation")
@@ -130,26 +139,22 @@ public class DefaultRenderersFactory implements RenderersFactory {
}
/**
- * @param context A {@link Context}.
- * @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.
+ * @deprecated Use {@link #DefaultRenderersFactory(Context)}, {@link
+ * #setExtensionRendererMode(int)} and {@link #setAllowedVideoJoiningTimeMs(long)}.
*/
+ @Deprecated
+ @SuppressWarnings("deprecation")
public DefaultRenderersFactory(
Context context,
@ExtensionRendererMode int extensionRendererMode,
long allowedVideoJoiningTimeMs) {
- this.context = context;
- this.extensionRendererMode = extensionRendererMode;
- this.allowedVideoJoiningTimeMs = allowedVideoJoiningTimeMs;
- this.drmSessionManager = null;
+ this(context, null, extensionRendererMode, allowedVideoJoiningTimeMs);
}
/**
- * @deprecated Use {@link #DefaultRenderersFactory(Context, int, long)} and pass {@link
- * DrmSessionManager} directly to {@link SimpleExoPlayer} or {@link ExoPlayerFactory}.
+ * @deprecated Use {@link #DefaultRenderersFactory(Context)}, {@link
+ * #setExtensionRendererMode(int)} and {@link #setAllowedVideoJoiningTimeMs(long)}, and pass
+ * {@link DrmSessionManager} directly to {@link SimpleExoPlayer} or {@link ExoPlayerFactory}.
*/
@Deprecated
public DefaultRenderersFactory(
@@ -161,6 +166,83 @@ public class DefaultRenderersFactory implements RenderersFactory {
this.extensionRendererMode = extensionRendererMode;
this.allowedVideoJoiningTimeMs = allowedVideoJoiningTimeMs;
this.drmSessionManager = drmSessionManager;
+ mediaCodecSelector = MediaCodecSelector.DEFAULT;
+ }
+
+ /**
+ * Sets 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.
+ *
+ * The default value is {@link #EXTENSION_RENDERER_MODE_OFF}.
+ *
+ * @param extensionRendererMode The extension renderer mode.
+ * @return This factory, for convenience.
+ */
+ public DefaultRenderersFactory setExtensionRendererMode(
+ @ExtensionRendererMode int extensionRendererMode) {
+ this.extensionRendererMode = extensionRendererMode;
+ return this;
+ }
+
+ /**
+ * Sets whether renderers are permitted to play clear regions of encrypted media prior to having
+ * obtained the keys necessary to decrypt encrypted regions of the media. For encrypted media that
+ * starts with a short clear region, this allows playback to begin in parallel with key
+ * acquisition, which can reduce startup latency.
+ *
+ *
The default value is {@code false}.
+ *
+ * @param playClearSamplesWithoutKeys Whether renderers are permitted to play clear regions of
+ * encrypted media prior to having obtained the keys necessary to decrypt encrypted regions of
+ * the media.
+ * @return This factory, for convenience.
+ */
+ public DefaultRenderersFactory setPlayClearSamplesWithoutKeys(
+ boolean playClearSamplesWithoutKeys) {
+ this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
+ return this;
+ }
+
+ /**
+ * Sets whether to enable fallback to lower-priority decoders if decoder initialization fails.
+ * This may result in using a decoder that is less efficient or slower than the primary decoder.
+ *
+ * @param enableDecoderFallback Whether to enable fallback to lower-priority decoders if decoder
+ * initialization fails.
+ * @return This factory, for convenience.
+ */
+ public DefaultRenderersFactory setEnableDecoderFallback(boolean enableDecoderFallback) {
+ this.enableDecoderFallback = enableDecoderFallback;
+ return this;
+ }
+
+ /**
+ * Sets a {@link MediaCodecSelector} for use by {@link MediaCodec} based renderers.
+ *
+ *
The default value is {@link MediaCodecSelector#DEFAULT}.
+ *
+ * @param mediaCodecSelector The {@link MediaCodecSelector}.
+ * @return This factory, for convenience.
+ */
+ public DefaultRenderersFactory setMediaCodecSelector(MediaCodecSelector mediaCodecSelector) {
+ this.mediaCodecSelector = mediaCodecSelector;
+ return this;
+ }
+
+ /**
+ * Sets the maximum duration for which video renderers can attempt to seamlessly join an ongoing
+ * playback.
+ *
+ *
The default value is {@link #DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS}.
+ *
+ * @param allowedVideoJoiningTimeMs The maximum duration for which video renderers can attempt to
+ * seamlessly join an ongoing playback, in milliseconds.
+ * @return This factory, for convenience.
+ */
+ public DefaultRenderersFactory setAllowedVideoJoiningTimeMs(long allowedVideoJoiningTimeMs) {
+ this.allowedVideoJoiningTimeMs = allowedVideoJoiningTimeMs;
+ return this;
}
@Override
@@ -175,44 +257,76 @@ public class DefaultRenderersFactory implements RenderersFactory {
drmSessionManager = this.drmSessionManager;
}
ArrayList renderersList = new ArrayList<>();
- buildVideoRenderers(context, drmSessionManager, allowedVideoJoiningTimeMs,
- eventHandler, videoRendererEventListener, extensionRendererMode, renderersList);
- buildAudioRenderers(context, drmSessionManager, buildAudioProcessors(),
- eventHandler, audioRendererEventListener, extensionRendererMode, renderersList);
+ buildVideoRenderers(
+ context,
+ extensionRendererMode,
+ mediaCodecSelector,
+ drmSessionManager,
+ playClearSamplesWithoutKeys,
+ enableDecoderFallback,
+ eventHandler,
+ videoRendererEventListener,
+ allowedVideoJoiningTimeMs,
+ renderersList);
+ buildAudioRenderers(
+ context,
+ extensionRendererMode,
+ mediaCodecSelector,
+ drmSessionManager,
+ playClearSamplesWithoutKeys,
+ enableDecoderFallback,
+ buildAudioProcessors(),
+ eventHandler,
+ audioRendererEventListener,
+ renderersList);
buildTextRenderers(context, textRendererOutput, eventHandler.getLooper(),
extensionRendererMode, renderersList);
buildMetadataRenderers(context, metadataRendererOutput, eventHandler.getLooper(),
extensionRendererMode, renderersList);
buildCameraMotionRenderers(context, extensionRendererMode, renderersList);
buildMiscellaneousRenderers(context, eventHandler, extensionRendererMode, renderersList);
- return renderersList.toArray(new Renderer[renderersList.size()]);
+ return renderersList.toArray(new Renderer[0]);
}
/**
* Builds video renderers for use by the player.
*
* @param context The {@link Context} associated with the player.
- * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player
- * will not be used for DRM protected playbacks.
- * @param allowedVideoJoiningTimeMs The maximum duration in milliseconds for which video
- * renderers can attempt to seamlessly join an ongoing playback.
+ * @param extensionRendererMode The extension renderer mode.
+ * @param mediaCodecSelector A decoder selector.
+ * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player will
+ * not be used for DRM protected playbacks.
+ * @param playClearSamplesWithoutKeys Whether renderers are permitted to play clear regions of
+ * encrypted media prior to having obtained the keys necessary to decrypt encrypted regions of
+ * the media.
+ * @param enableDecoderFallback Whether to enable fallback to lower-priority decoders if decoder
+ * initialization fails. This may result in using a decoder that is slower/less efficient than
+ * the primary decoder.
* @param eventHandler A handler associated with the main thread's looper.
* @param eventListener An event listener.
- * @param extensionRendererMode The extension renderer mode.
+ * @param allowedVideoJoiningTimeMs The maximum duration for which video renderers can attempt to
+ * seamlessly join an ongoing playback, in milliseconds.
* @param out An array to which the built renderers should be appended.
*/
- protected void buildVideoRenderers(Context context,
+ protected void buildVideoRenderers(
+ Context context,
+ @ExtensionRendererMode int extensionRendererMode,
+ MediaCodecSelector mediaCodecSelector,
@Nullable DrmSessionManager drmSessionManager,
- long allowedVideoJoiningTimeMs, Handler eventHandler,
- VideoRendererEventListener eventListener, @ExtensionRendererMode int extensionRendererMode,
+ boolean playClearSamplesWithoutKeys,
+ boolean enableDecoderFallback,
+ Handler eventHandler,
+ VideoRendererEventListener eventListener,
+ long allowedVideoJoiningTimeMs,
ArrayList out) {
out.add(
new MediaCodecVideoRenderer(
context,
- MediaCodecSelector.DEFAULT,
+ mediaCodecSelector,
allowedVideoJoiningTimeMs,
drmSessionManager,
- /* playClearSamplesWithoutKeys= */ false,
+ playClearSamplesWithoutKeys,
+ enableDecoderFallback,
eventHandler,
eventListener,
MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY));
@@ -231,7 +345,6 @@ public class DefaultRenderersFactory implements RenderersFactory {
Class> clazz = Class.forName("com.google.android.exoplayer2.ext.vp9.LibvpxVideoRenderer");
Constructor> constructor =
clazz.getConstructor(
- boolean.class,
long.class,
android.os.Handler.class,
com.google.android.exoplayer2.video.VideoRendererEventListener.class,
@@ -240,7 +353,6 @@ public class DefaultRenderersFactory implements RenderersFactory {
Renderer renderer =
(Renderer)
constructor.newInstance(
- true,
allowedVideoJoiningTimeMs,
eventHandler,
eventListener,
@@ -259,30 +371,43 @@ public class DefaultRenderersFactory implements RenderersFactory {
* Builds audio renderers for use by the player.
*
* @param context The {@link Context} associated with the player.
- * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player
- * will not be used for DRM protected playbacks.
- * @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio
- * buffers before output. May be empty.
+ * @param extensionRendererMode The extension renderer mode.
+ * @param mediaCodecSelector A decoder selector.
+ * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player will
+ * not be used for DRM protected playbacks.
+ * @param playClearSamplesWithoutKeys Whether renderers are permitted to play clear regions of
+ * encrypted media prior to having obtained the keys necessary to decrypt encrypted regions of
+ * the media.
+ * @param enableDecoderFallback Whether to enable fallback to lower-priority decoders if decoder
+ * initialization fails. This may result in using a decoder that is slower/less efficient than
+ * the primary decoder.
+ * @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio buffers
+ * before output. May be empty.
* @param eventHandler A handler to use when invoking event listeners and outputs.
* @param eventListener An event listener.
- * @param extensionRendererMode The extension renderer mode.
* @param out An array to which the built renderers should be appended.
*/
- protected void buildAudioRenderers(Context context,
+ protected void buildAudioRenderers(
+ Context context,
+ @ExtensionRendererMode int extensionRendererMode,
+ MediaCodecSelector mediaCodecSelector,
@Nullable DrmSessionManager drmSessionManager,
- AudioProcessor[] audioProcessors, Handler eventHandler,
- AudioRendererEventListener eventListener, @ExtensionRendererMode int extensionRendererMode,
+ boolean playClearSamplesWithoutKeys,
+ boolean enableDecoderFallback,
+ AudioProcessor[] audioProcessors,
+ Handler eventHandler,
+ AudioRendererEventListener eventListener,
ArrayList out) {
out.add(
new MediaCodecAudioRenderer(
context,
- MediaCodecSelector.DEFAULT,
+ mediaCodecSelector,
drmSessionManager,
- /* playClearSamplesWithoutKeys= */ false,
+ playClearSamplesWithoutKeys,
+ enableDecoderFallback,
eventHandler,
eventListener,
- AudioCapabilities.getCapabilities(context),
- audioProcessors));
+ new DefaultAudioSink(AudioCapabilities.getCapabilities(context), audioProcessors)));
if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) {
return;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java
index ba00d1163f..49aacd9638 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java
@@ -15,10 +15,13 @@
*/
package com.google.android.exoplayer2;
-import android.support.annotation.IntDef;
+import android.os.SystemClock;
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.util.Assertions;
import java.io.IOException;
+import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -29,10 +32,12 @@ public final class ExoPlaybackException extends Exception {
/**
* The type of source that produced the error. One of {@link #TYPE_SOURCE}, {@link #TYPE_RENDERER}
- * or {@link #TYPE_UNEXPECTED}.
+ * {@link #TYPE_UNEXPECTED}, {@link #TYPE_REMOTE} or {@link #TYPE_OUT_OF_MEMORY}. Note that new
+ * types may be added in the future and error handling should handle unknown type values.
*/
+ @Documented
@Retention(RetentionPolicy.SOURCE)
- @IntDef({TYPE_SOURCE, TYPE_RENDERER, TYPE_UNEXPECTED})
+ @IntDef({TYPE_SOURCE, TYPE_RENDERER, TYPE_UNEXPECTED, TYPE_REMOTE, TYPE_OUT_OF_MEMORY})
public @interface Type {}
/**
* The error occurred loading data from a {@link MediaSource}.
@@ -52,11 +57,16 @@ public final class ExoPlaybackException extends Exception {
* Call {@link #getUnexpectedException()} to retrieve the underlying cause.
*/
public static final int TYPE_UNEXPECTED = 2;
-
/**
- * The type of the playback failure. One of {@link #TYPE_SOURCE}, {@link #TYPE_RENDERER} and
- * {@link #TYPE_UNEXPECTED}.
+ * The error occurred in a remote component.
+ *
+ * Call {@link #getMessage()} to retrieve the message associated with the error.
*/
+ public static final int TYPE_REMOTE = 3;
+ /** The error was an {@link OutOfMemoryError}. */
+ public static final int TYPE_OUT_OF_MEMORY = 4;
+
+ /** The {@link Type} of the playback failure. */
@Type public final int type;
/**
@@ -64,6 +74,21 @@ public final class ExoPlaybackException extends Exception {
*/
public final int rendererIndex;
+ /** The value of {@link SystemClock#elapsedRealtime()} when this exception was created. */
+ public final long timestampMs;
+
+ @Nullable private final Throwable cause;
+
+ /**
+ * Creates an instance of type {@link #TYPE_SOURCE}.
+ *
+ * @param cause The cause of the failure.
+ * @return The created instance.
+ */
+ public static ExoPlaybackException createForSource(IOException cause) {
+ return new ExoPlaybackException(TYPE_SOURCE, cause, /* rendererIndex= */ C.INDEX_UNSET);
+ }
+
/**
* Creates an instance of type {@link #TYPE_RENDERER}.
*
@@ -72,17 +97,7 @@ public final class ExoPlaybackException extends Exception {
* @return The created instance.
*/
public static ExoPlaybackException createForRenderer(Exception cause, int rendererIndex) {
- return new ExoPlaybackException(TYPE_RENDERER, null, cause, rendererIndex);
- }
-
- /**
- * Creates an instance of type {@link #TYPE_SOURCE}.
- *
- * @param cause The cause of the failure.
- * @return The created instance.
- */
- public static ExoPlaybackException createForSource(IOException cause) {
- return new ExoPlaybackException(TYPE_SOURCE, null, cause, C.INDEX_UNSET);
+ return new ExoPlaybackException(TYPE_RENDERER, cause, rendererIndex);
}
/**
@@ -91,15 +106,44 @@ public final class ExoPlaybackException extends Exception {
* @param cause The cause of the failure.
* @return The created instance.
*/
- /* package */ static ExoPlaybackException createForUnexpected(RuntimeException cause) {
- return new ExoPlaybackException(TYPE_UNEXPECTED, null, cause, C.INDEX_UNSET);
+ public static ExoPlaybackException createForUnexpected(RuntimeException cause) {
+ return new ExoPlaybackException(TYPE_UNEXPECTED, cause, /* rendererIndex= */ C.INDEX_UNSET);
}
- private ExoPlaybackException(@Type int type, String message, Throwable cause,
- int rendererIndex) {
- super(message, cause);
+ /**
+ * Creates an instance of type {@link #TYPE_REMOTE}.
+ *
+ * @param message The message associated with the error.
+ * @return The created instance.
+ */
+ public static ExoPlaybackException createForRemote(String message) {
+ return new ExoPlaybackException(TYPE_REMOTE, message);
+ }
+
+ /**
+ * Creates an instance of type {@link #TYPE_OUT_OF_MEMORY}.
+ *
+ * @param cause The cause of the failure.
+ * @return The created instance.
+ */
+ public static ExoPlaybackException createForOutOfMemoryError(OutOfMemoryError cause) {
+ return new ExoPlaybackException(TYPE_OUT_OF_MEMORY, cause, /* rendererIndex= */ C.INDEX_UNSET);
+ }
+
+ private ExoPlaybackException(@Type int type, Throwable cause, int rendererIndex) {
+ super(cause);
this.type = type;
+ this.cause = cause;
this.rendererIndex = rendererIndex;
+ timestampMs = SystemClock.elapsedRealtime();
+ }
+
+ private ExoPlaybackException(@Type int type, String message) {
+ super(message);
+ this.type = type;
+ rendererIndex = C.INDEX_UNSET;
+ cause = null;
+ timestampMs = SystemClock.elapsedRealtime();
}
/**
@@ -109,7 +153,7 @@ public final class ExoPlaybackException extends Exception {
*/
public IOException getSourceException() {
Assertions.checkState(type == TYPE_SOURCE);
- return (IOException) getCause();
+ return (IOException) Assertions.checkNotNull(cause);
}
/**
@@ -119,7 +163,7 @@ public final class ExoPlaybackException extends Exception {
*/
public Exception getRendererException() {
Assertions.checkState(type == TYPE_RENDERER);
- return (Exception) getCause();
+ return (Exception) Assertions.checkNotNull(cause);
}
/**
@@ -129,7 +173,16 @@ public final class ExoPlaybackException extends Exception {
*/
public RuntimeException getUnexpectedException() {
Assertions.checkState(type == TYPE_UNEXPECTED);
- return (RuntimeException) getCause();
+ return (RuntimeException) Assertions.checkNotNull(cause);
}
+ /**
+ * Retrieves the underlying error when {@link #type} is {@link #TYPE_OUT_OF_MEMORY}.
+ *
+ * @throws IllegalStateException If {@link #type} is not {@link #TYPE_OUT_OF_MEMORY}.
+ */
+ public OutOfMemoryError getOutOfMemoryError() {
+ Assertions.checkState(type == TYPE_OUT_OF_MEMORY);
+ return (OutOfMemoryError) Assertions.checkNotNull(cause);
+ }
}
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 452c1043a3..ee29af9c99 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
@@ -16,15 +16,15 @@
package com.google.android.exoplayer2;
import android.os.Looper;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
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.ExtractorMediaSource;
import com.google.android.exoplayer2.source.LoopingMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MergingMediaSource;
+import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.source.SingleSampleMediaSource;
import com.google.android.exoplayer2.text.TextRenderer;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
@@ -48,7 +48,7 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
*
A {@link MediaSource} that defines the media to be played, loads the media, and from
* which the loaded media can be read. A MediaSource is injected via {@link
* #prepare(MediaSource)} at the start of playback. The library modules provide default
- * implementations for regular media files ({@link ExtractorMediaSource}), DASH
+ * implementations for progressive media files ({@link ProgressiveMediaSource}), DASH
* (DashMediaSource), SmoothStreaming (SsMediaSource) and HLS (HlsMediaSource), an
* implementation for loading single media samples ({@link SingleSampleMediaSource}) that's
* most often used for side-loaded subtitle files, and implementations for building more
@@ -89,10 +89,15 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
* model">
*
*
- * ExoPlayer instances must be accessed from the thread associated with {@link
- * #getApplicationLooper()}. This Looper can be specified when creating the player, or this is
- * the Looper of the thread the player is created on, or the Looper of the application's main
- * thread if the player is created on a thread without Looper.
+ * ExoPlayer instances must be accessed from a single application thread. For the vast
+ * majority of cases this should be the application's main thread. Using the application's
+ * main thread is also a requirement when using ExoPlayer's UI components or the IMA
+ * extension. The thread on which an ExoPlayer instance must be accessed can be explicitly
+ * specified by passing a `Looper` when creating the player. If no `Looper` is specified, then
+ * the `Looper` of the thread that the player is created on is used, or if that thread does
+ * not have a `Looper`, the `Looper` of the application's main thread is used. In all cases
+ * the `Looper` of the thread from which the player must be accessed can be queried using
+ * {@link #getApplicationLooper()}.
* Registered listeners are called on the thread associated with {@link
* #getApplicationLooper()}. Note that this means registered listeners are called on the same
* thread which must be used to access the player.
@@ -112,97 +117,24 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
*/
public interface ExoPlayer extends Player {
- /**
- * @deprecated Use {@link Player.EventListener} instead.
- */
- @Deprecated
- interface EventListener extends Player.EventListener {}
-
- /** @deprecated Use {@link PlayerMessage.Target} instead. */
- @Deprecated
- interface ExoPlayerComponent extends PlayerMessage.Target {}
-
- /** @deprecated Use {@link PlayerMessage} instead. */
- @Deprecated
- final class ExoPlayerMessage {
-
- /** The target to receive the message. */
- public final PlayerMessage.Target target;
- /** The type of the message. */
- public final int messageType;
- /** The message. */
- public final Object message;
-
- /** @deprecated Use {@link ExoPlayer#createMessage(PlayerMessage.Target)} instead. */
- @Deprecated
- public ExoPlayerMessage(PlayerMessage.Target target, int messageType, Object message) {
- this.target = target;
- this.messageType = messageType;
- this.message = message;
- }
- }
-
- /**
- * @deprecated Use {@link Player#STATE_IDLE} instead.
- */
- @Deprecated
- int STATE_IDLE = Player.STATE_IDLE;
- /**
- * @deprecated Use {@link Player#STATE_BUFFERING} instead.
- */
- @Deprecated
- int STATE_BUFFERING = Player.STATE_BUFFERING;
- /**
- * @deprecated Use {@link Player#STATE_READY} instead.
- */
- @Deprecated
- int STATE_READY = Player.STATE_READY;
- /**
- * @deprecated Use {@link Player#STATE_ENDED} instead.
- */
- @Deprecated
- int STATE_ENDED = Player.STATE_ENDED;
-
- /**
- * @deprecated Use {@link Player#REPEAT_MODE_OFF} instead.
- */
- @Deprecated
- @RepeatMode int REPEAT_MODE_OFF = Player.REPEAT_MODE_OFF;
- /**
- * @deprecated Use {@link Player#REPEAT_MODE_ONE} instead.
- */
- @Deprecated
- @RepeatMode int REPEAT_MODE_ONE = Player.REPEAT_MODE_ONE;
- /**
- * @deprecated Use {@link Player#REPEAT_MODE_ALL} instead.
- */
- @Deprecated
- @RepeatMode int REPEAT_MODE_ALL = Player.REPEAT_MODE_ALL;
-
/** Returns the {@link Looper} associated with the playback thread. */
Looper getPlaybackLooper();
/**
- * Returns the {@link Looper} associated with the application thread that's used to access the
- * player and on which player events are received.
+ * Retries a failed or stopped playback. Does nothing if the player has been reset, or if playback
+ * has not failed or been stopped.
*/
- Looper getApplicationLooper();
+ void retry();
/**
* Prepares the player to play the provided {@link MediaSource}. Equivalent to
* {@code prepare(mediaSource, true, true)}.
- *
- * Note: {@link MediaSource} instances are not designed to be re-used. If you want to prepare a
- * player more than once with the same piece of media, use a new instance each time.
*/
void prepare(MediaSource mediaSource);
/**
* Prepares the player to play the provided {@link MediaSource}, optionally resetting the playback
* position the default position in the first {@link Timeline.Window}.
- *
- * Note: {@link MediaSource} instances are not designed to be re-used. If you want to prepare a
- * player more than once with the same piece of media, use a new instance each time.
*
* @param mediaSource The {@link MediaSource} to play.
* @param resetPosition Whether the playback position should be reset to the default position in
@@ -225,19 +157,6 @@ public interface ExoPlayer extends Player {
*/
PlayerMessage createMessage(PlayerMessage.Target target);
- /** @deprecated Use {@link #createMessage(PlayerMessage.Target)} instead. */
- @Deprecated
- @SuppressWarnings("deprecation")
- void sendMessages(ExoPlayerMessage... messages);
-
- /**
- * @deprecated Use {@link #createMessage(PlayerMessage.Target)} with {@link
- * PlayerMessage#blockUntilDelivered()}.
- */
- @Deprecated
- @SuppressWarnings("deprecation")
- void blockingSendMessages(ExoPlayerMessage... messages);
-
/**
* Sets the parameters that control how seek operations are performed.
*
@@ -247,4 +166,34 @@ public interface ExoPlayer extends Player {
/** Returns the currently active {@link SeekParameters} of the player. */
SeekParameters getSeekParameters();
+
+ /**
+ * Sets whether the player is allowed to keep holding limited resources such as video decoders,
+ * even when in the idle state. By doing so, the player may be able to reduce latency when
+ * starting to play another piece of content for which the same resources are required.
+ *
+ *
This mode should be used with caution, since holding limited resources may prevent other
+ * players of media components from acquiring them. It should only be enabled when both
+ * of the following conditions are true:
+ *
+ *
+ * The application that owns the player is in the foreground.
+ * The player is used in a way that may benefit from foreground mode. For this to be true,
+ * the same player instance must be used to play multiple pieces of content, and there must
+ * be gaps between the playbacks (i.e. {@link #stop} is called to halt one playback, and
+ * {@link #prepare} is called some time later to start a new one).
+ *
+ *
+ * Note that foreground mode is not useful for switching between content without gaps
+ * between the playbacks. For this use case {@link #stop} does not need to be called, and simply
+ * calling {@link #prepare} for the new media will cause limited resources to be retained even if
+ * foreground mode is not enabled.
+ *
+ *
If foreground mode is enabled, it's the application's responsibility to disable it when the
+ * conditions described above no longer hold.
+ *
+ * @param foregroundMode Whether the player is allowed to keep limited resources even when in the
+ * idle state.
+ */
+ void setForegroundMode(boolean foregroundMode);
}
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 b00a485843..9168f1bd76 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,11 +17,11 @@ package com.google.android.exoplayer2;
import android.content.Context;
import android.os.Looper;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.analytics.AnalyticsCollector;
-import com.google.android.exoplayer2.audio.AudioAttributes;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
+import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
@@ -37,44 +37,6 @@ public final class ExoPlayerFactory {
private ExoPlayerFactory() {}
- /**
- * Creates a {@link SimpleExoPlayer} instance.
- *
- * @param context A {@link Context}.
- * @param trackSelector The {@link TrackSelector} that will be used by the instance.
- * @param loadControl The {@link LoadControl} that will be used by the instance.
- * @deprecated Use {@link #newSimpleInstance(Context, RenderersFactory, TrackSelector,
- * LoadControl)}.
- */
- @Deprecated
- public static SimpleExoPlayer newSimpleInstance(
- Context context, TrackSelector trackSelector, LoadControl loadControl) {
- RenderersFactory renderersFactory = new DefaultRenderersFactory(context);
- return newSimpleInstance(context, renderersFactory, trackSelector, loadControl);
- }
-
- /**
- * Creates a {@link SimpleExoPlayer} instance. Available extension renderers are not used.
- *
- * @param context A {@link Context}.
- * @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.
- * @deprecated Use {@link #newSimpleInstance(Context, RenderersFactory, TrackSelector,
- * LoadControl)}.
- */
- @Deprecated
- public static SimpleExoPlayer newSimpleInstance(
- Context context,
- TrackSelector trackSelector,
- LoadControl loadControl,
- @Nullable DrmSessionManager drmSessionManager) {
- RenderersFactory renderersFactory = new DefaultRenderersFactory(context);
- return newSimpleInstance(
- context, renderersFactory, trackSelector, loadControl, drmSessionManager);
- }
-
/**
* Creates a {@link SimpleExoPlayer} instance.
*
@@ -87,7 +49,7 @@ public final class ExoPlayerFactory {
* extension renderers are used. Note that extensions must be included in the application
* build for them to be considered available.
* @deprecated Use {@link #newSimpleInstance(Context, RenderersFactory, TrackSelector,
- * LoadControl)}.
+ * LoadControl, DrmSessionManager)}.
*/
@Deprecated
public static SimpleExoPlayer newSimpleInstance(
@@ -96,7 +58,8 @@ public final class ExoPlayerFactory {
LoadControl loadControl,
@Nullable DrmSessionManager drmSessionManager,
@DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode) {
- RenderersFactory renderersFactory = new DefaultRenderersFactory(context, extensionRendererMode);
+ RenderersFactory renderersFactory =
+ new DefaultRenderersFactory(context).setExtensionRendererMode(extensionRendererMode);
return newSimpleInstance(
context, renderersFactory, trackSelector, loadControl, drmSessionManager);
}
@@ -115,7 +78,7 @@ public final class ExoPlayerFactory {
* @param allowedVideoJoiningTimeMs The maximum duration for which a video renderer can attempt to
* seamlessly join an ongoing playback.
* @deprecated Use {@link #newSimpleInstance(Context, RenderersFactory, TrackSelector,
- * LoadControl)}.
+ * LoadControl, DrmSessionManager)}.
*/
@Deprecated
public static SimpleExoPlayer newSimpleInstance(
@@ -126,11 +89,22 @@ public final class ExoPlayerFactory {
@DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode,
long allowedVideoJoiningTimeMs) {
RenderersFactory renderersFactory =
- new DefaultRenderersFactory(context, extensionRendererMode, allowedVideoJoiningTimeMs);
+ new DefaultRenderersFactory(context)
+ .setExtensionRendererMode(extensionRendererMode)
+ .setAllowedVideoJoiningTimeMs(allowedVideoJoiningTimeMs);
return newSimpleInstance(
context, renderersFactory, trackSelector, loadControl, drmSessionManager);
}
+ /**
+ * Creates a {@link SimpleExoPlayer} instance.
+ *
+ * @param context A {@link Context}.
+ */
+ public static SimpleExoPlayer newSimpleInstance(Context context) {
+ return newSimpleInstance(context, new DefaultTrackSelector(context));
+ }
+
/**
* Creates a {@link SimpleExoPlayer} instance.
*
@@ -141,23 +115,6 @@ public final class ExoPlayerFactory {
return newSimpleInstance(context, new DefaultRenderersFactory(context), trackSelector);
}
- /**
- * 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.
- * @deprecated Use {@link #newSimpleInstance(Context, RenderersFactory, TrackSelector)}. The use
- * of {@link SimpleExoPlayer#setAudioAttributes(AudioAttributes, boolean)} to manage audio
- * focus will be unavailable for the {@link SimpleExoPlayer} returned by this method.
- */
- @Deprecated
- @SuppressWarnings("nullness:argument.type.incompatible")
- public static SimpleExoPlayer newSimpleInstance(
- RenderersFactory renderersFactory, TrackSelector trackSelector) {
- return newSimpleInstance(
- /* context= */ null, renderersFactory, trackSelector, new DefaultLoadControl());
- }
-
/**
* Creates a {@link SimpleExoPlayer} instance.
*
@@ -170,6 +127,38 @@ public final class ExoPlayerFactory {
return newSimpleInstance(context, renderersFactory, trackSelector, new DefaultLoadControl());
}
+ /**
+ * Creates a {@link SimpleExoPlayer} instance.
+ *
+ * @param context A {@link Context}.
+ * @param trackSelector The {@link TrackSelector} that will be used by the instance.
+ * @param loadControl The {@link LoadControl} that will be used by the instance.
+ */
+ public static SimpleExoPlayer newSimpleInstance(
+ Context context, TrackSelector trackSelector, LoadControl loadControl) {
+ RenderersFactory renderersFactory = new DefaultRenderersFactory(context);
+ return newSimpleInstance(context, renderersFactory, trackSelector, loadControl);
+ }
+
+ /**
+ * Creates a {@link SimpleExoPlayer} instance. Available extension renderers are not used.
+ *
+ * @param context A {@link Context}.
+ * @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(
+ Context context,
+ TrackSelector trackSelector,
+ LoadControl loadControl,
+ @Nullable DrmSessionManager drmSessionManager) {
+ RenderersFactory renderersFactory = new DefaultRenderersFactory(context);
+ return newSimpleInstance(
+ context, renderersFactory, trackSelector, loadControl, drmSessionManager);
+ }
+
/**
* Creates a {@link SimpleExoPlayer} instance.
*
@@ -255,7 +244,7 @@ public final class ExoPlayerFactory {
loadControl,
drmSessionManager,
bandwidthMeter,
- new AnalyticsCollector.Factory(),
+ new AnalyticsCollector(Clock.DEFAULT),
Util.getLooper());
}
@@ -268,8 +257,8 @@ public final class ExoPlayerFactory {
* @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 analyticsCollector The {@link AnalyticsCollector} that will collect and forward all
+ * player events.
*/
public static SimpleExoPlayer newSimpleInstance(
Context context,
@@ -277,14 +266,14 @@ public final class ExoPlayerFactory {
TrackSelector trackSelector,
LoadControl loadControl,
@Nullable DrmSessionManager drmSessionManager,
- AnalyticsCollector.Factory analyticsCollectorFactory) {
+ AnalyticsCollector analyticsCollector) {
return newSimpleInstance(
context,
renderersFactory,
trackSelector,
loadControl,
drmSessionManager,
- analyticsCollectorFactory,
+ analyticsCollector,
Util.getLooper());
}
@@ -313,7 +302,7 @@ public final class ExoPlayerFactory {
trackSelector,
loadControl,
drmSessionManager,
- new AnalyticsCollector.Factory(),
+ new AnalyticsCollector(Clock.DEFAULT),
looper);
}
@@ -326,8 +315,8 @@ public final class ExoPlayerFactory {
* @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 analyticsCollector The {@link AnalyticsCollector} that will collect and forward all
+ * player events.
* @param looper The {@link Looper} which must be used for all calls to the player and which is
* used to call listeners on.
*/
@@ -337,7 +326,7 @@ public final class ExoPlayerFactory {
TrackSelector trackSelector,
LoadControl loadControl,
@Nullable DrmSessionManager drmSessionManager,
- AnalyticsCollector.Factory analyticsCollectorFactory,
+ AnalyticsCollector analyticsCollector,
Looper looper) {
return newSimpleInstance(
context,
@@ -345,8 +334,8 @@ public final class ExoPlayerFactory {
trackSelector,
loadControl,
drmSessionManager,
- getDefaultBandwidthMeter(),
- analyticsCollectorFactory,
+ getDefaultBandwidthMeter(context),
+ analyticsCollector,
looper);
}
@@ -359,8 +348,8 @@ public final class ExoPlayerFactory {
* @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 analyticsCollector The {@link AnalyticsCollector} that will collect and forward all
+ * player events.
* @param looper The {@link Looper} which must be used for all calls to the player and which is
* used to call listeners on.
*/
@@ -371,7 +360,7 @@ public final class ExoPlayerFactory {
LoadControl loadControl,
@Nullable DrmSessionManager drmSessionManager,
BandwidthMeter bandwidthMeter,
- AnalyticsCollector.Factory analyticsCollectorFactory,
+ AnalyticsCollector analyticsCollector,
Looper looper) {
return new SimpleExoPlayer(
context,
@@ -380,35 +369,39 @@ public final class ExoPlayerFactory {
loadControl,
drmSessionManager,
bandwidthMeter,
- analyticsCollectorFactory,
+ analyticsCollector,
looper);
}
/**
* Creates an {@link ExoPlayer} instance.
*
+ * @param context A {@link Context}.
* @param renderers The {@link Renderer}s that will be used by the instance.
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
*/
- public static ExoPlayer newInstance(Renderer[] renderers, TrackSelector trackSelector) {
- return newInstance(renderers, trackSelector, new DefaultLoadControl());
+ public static ExoPlayer newInstance(
+ Context context, Renderer[] renderers, TrackSelector trackSelector) {
+ return newInstance(context, renderers, trackSelector, new DefaultLoadControl());
}
/**
* Creates an {@link ExoPlayer} instance.
*
+ * @param context A {@link Context}.
* @param renderers The {@link Renderer}s that will 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.
*/
- public static ExoPlayer newInstance(Renderer[] renderers, TrackSelector trackSelector,
- LoadControl loadControl) {
- return newInstance(renderers, trackSelector, loadControl, Util.getLooper());
+ public static ExoPlayer newInstance(
+ Context context, Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl) {
+ return newInstance(context, renderers, trackSelector, loadControl, Util.getLooper());
}
/**
* Creates an {@link ExoPlayer} instance.
*
+ * @param context A {@link Context}.
* @param renderers The {@link Renderer}s that will 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.
@@ -416,13 +409,19 @@ public final class ExoPlayerFactory {
* used to call listeners on.
*/
public static ExoPlayer newInstance(
- Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl, Looper looper) {
- return newInstance(renderers, trackSelector, loadControl, getDefaultBandwidthMeter(), looper);
+ Context context,
+ Renderer[] renderers,
+ TrackSelector trackSelector,
+ LoadControl loadControl,
+ Looper looper) {
+ return newInstance(
+ context, renderers, trackSelector, loadControl, getDefaultBandwidthMeter(context), looper);
}
/**
* Creates an {@link ExoPlayer} instance.
*
+ * @param context A {@link Context}.
* @param renderers The {@link Renderer}s that will 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.
@@ -430,7 +429,9 @@ public final class ExoPlayerFactory {
* @param looper The {@link Looper} which must be used for all calls to the player and which is
* used to call listeners on.
*/
+ @SuppressWarnings("unused")
public static ExoPlayer newInstance(
+ Context context,
Renderer[] renderers,
TrackSelector trackSelector,
LoadControl loadControl,
@@ -440,9 +441,9 @@ public final class ExoPlayerFactory {
renderers, trackSelector, loadControl, bandwidthMeter, Clock.DEFAULT, looper);
}
- private static synchronized BandwidthMeter getDefaultBandwidthMeter() {
+ private static synchronized BandwidthMeter getDefaultBandwidthMeter(Context context) {
if (singletonBandwidthMeter == null) {
- singletonBandwidthMeter = new DefaultBandwidthMeter.Builder().build();
+ singletonBandwidthMeter = new DefaultBandwidthMeter.Builder(context).build();
}
return singletonBandwidthMeter;
}
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 5e0dd905b9..e99429d3b2 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
@@ -19,8 +19,7 @@ import android.annotation.SuppressLint;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
-import android.support.annotation.Nullable;
-import android.util.Log;
+import androidx.annotation.Nullable;
import android.util.Pair;
import com.google.android.exoplayer2.PlayerMessage.Target;
import com.google.android.exoplayer2.source.MediaSource;
@@ -33,17 +32,13 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
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.util.Log;
import com.google.android.exoplayer2.util.Util;
import java.util.ArrayDeque;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Set;
-import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.concurrent.CopyOnWriteArrayList;
-/**
- * An {@link ExoPlayer} implementation. Instances can be obtained from {@link ExoPlayerFactory}.
- */
-/* package */ final class ExoPlayerImpl implements ExoPlayer {
+/** An {@link ExoPlayer} implementation. Instances can be obtained from {@link ExoPlayerFactory}. */
+/* package */ final class ExoPlayerImpl extends BasePlayer implements ExoPlayer {
private static final String TAG = "ExoPlayerImpl";
@@ -51,7 +46,8 @@ import java.util.concurrent.CopyOnWriteArraySet;
* This empty track selector result can only be used for {@link PlaybackInfo#trackSelectorResult}
* when the player does not have any track selection made (such as when player is reset, or when
* player seeks to an unprepared period). It will not be used as result of any {@link
- * TrackSelector#selectTracks(RendererCapabilities[], TrackGroupArray)} operation.
+ * TrackSelector#selectTracks(RendererCapabilities[], TrackGroupArray, MediaPeriodId, Timeline)}
+ * operation.
*/
/* package */ final TrackSelectorResult emptyTrackSelectorResult;
@@ -60,11 +56,11 @@ import java.util.concurrent.CopyOnWriteArraySet;
private final Handler eventHandler;
private final ExoPlayerImplInternal internalPlayer;
private final Handler internalPlayerHandler;
- private final CopyOnWriteArraySet listeners;
- private final Timeline.Window window;
+ private final CopyOnWriteArrayList listeners;
private final Timeline.Period period;
- private final ArrayDeque pendingPlaybackInfoUpdates;
+ private final ArrayDeque pendingListenerNotifications;
+ private MediaSource mediaSource;
private boolean playWhenReady;
private boolean internalPlayWhenReady;
private @RepeatMode int repeatMode;
@@ -72,9 +68,11 @@ import java.util.concurrent.CopyOnWriteArraySet;
private int pendingOperationAcks;
private boolean hasPendingPrepare;
private boolean hasPendingSeek;
+ private boolean foregroundMode;
+ private int pendingSetPlaybackParametersAcks;
private PlaybackParameters playbackParameters;
private SeekParameters seekParameters;
- private @Nullable ExoPlaybackException playbackError;
+ @Nullable private ExoPlaybackException playbackError;
// Playback information when there is no pending seek/set source operation.
private PlaybackInfo playbackInfo;
@@ -111,13 +109,12 @@ import java.util.concurrent.CopyOnWriteArraySet;
this.playWhenReady = false;
this.repeatMode = Player.REPEAT_MODE_OFF;
this.shuffleModeEnabled = false;
- this.listeners = new CopyOnWriteArraySet<>();
+ this.listeners = new CopyOnWriteArrayList<>();
emptyTrackSelectorResult =
new TrackSelectorResult(
new RendererConfiguration[renderers.length],
new TrackSelection[renderers.length],
null);
- window = new Timeline.Window();
period = new Timeline.Period();
playbackParameters = PlaybackParameters.DEFAULT;
seekParameters = SeekParameters.DEFAULT;
@@ -129,7 +126,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
}
};
playbackInfo = PlaybackInfo.createDummy(/* startPositionUs= */ 0, emptyTrackSelectorResult);
- pendingPlaybackInfoUpdates = new ArrayDeque<>();
+ pendingListenerNotifications = new ArrayDeque<>();
internalPlayer =
new ExoPlayerImplInternal(
renderers,
@@ -141,26 +138,34 @@ import java.util.concurrent.CopyOnWriteArraySet;
repeatMode,
shuffleModeEnabled,
eventHandler,
- this,
clock);
internalPlayerHandler = new Handler(internalPlayer.getPlaybackLooper());
}
@Override
+ @Nullable
public AudioComponent getAudioComponent() {
return null;
}
@Override
+ @Nullable
public VideoComponent getVideoComponent() {
return null;
}
@Override
+ @Nullable
public TextComponent getTextComponent() {
return null;
}
+ @Override
+ @Nullable
+ public MetadataComponent getMetadataComponent() {
+ return null;
+ }
+
@Override
public Looper getPlaybackLooper() {
return internalPlayer.getPlaybackLooper();
@@ -173,32 +178,48 @@ import java.util.concurrent.CopyOnWriteArraySet;
@Override
public void addListener(Player.EventListener listener) {
- listeners.add(listener);
+ listeners.addIfAbsent(new ListenerHolder(listener));
}
@Override
public void removeListener(Player.EventListener listener) {
- listeners.remove(listener);
+ for (ListenerHolder listenerHolder : listeners) {
+ if (listenerHolder.listener.equals(listener)) {
+ listenerHolder.release();
+ listeners.remove(listenerHolder);
+ }
+ }
}
@Override
+ @Player.State
public int getPlaybackState() {
return playbackInfo.playbackState;
}
@Override
- public @Nullable ExoPlaybackException getPlaybackError() {
+ @Nullable
+ public ExoPlaybackException getPlaybackError() {
return playbackError;
}
+ @Override
+ public void retry() {
+ if (mediaSource != null
+ && (playbackError != null || playbackInfo.playbackState == Player.STATE_IDLE)) {
+ prepare(mediaSource, /* resetPosition= */ false, /* resetState= */ false);
+ }
+ }
+
@Override
public void prepare(MediaSource mediaSource) {
- prepare(mediaSource, true, true);
+ prepare(mediaSource, /* resetPosition= */ true, /* resetState= */ true);
}
@Override
public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
playbackError = null;
+ this.mediaSource = mediaSource;
PlaybackInfo playbackInfo =
getResetPlaybackInfo(
resetPosition, resetState, /* playbackState= */ Player.STATE_BUFFERING);
@@ -214,8 +235,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
/* positionDiscontinuity= */ false,
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
TIMELINE_CHANGE_REASON_RESET,
- /* seekProcessed= */ false,
- /* playWhenReadyChanged= */ false);
+ /* seekProcessed= */ false);
}
@Override
@@ -231,13 +251,8 @@ import java.util.concurrent.CopyOnWriteArraySet;
}
if (this.playWhenReady != playWhenReady) {
this.playWhenReady = playWhenReady;
- updatePlaybackInfo(
- playbackInfo,
- /* positionDiscontinuity= */ false,
- /* ignored */ DISCONTINUITY_REASON_INTERNAL,
- /* ignored */ TIMELINE_CHANGE_REASON_RESET,
- /* seekProcessed= */ false,
- /* playWhenReadyChanged= */ true);
+ int playbackState = playbackInfo.playbackState;
+ notifyListeners(listener -> listener.onPlayerStateChanged(playWhenReady, playbackState));
}
}
@@ -251,9 +266,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
if (this.repeatMode != repeatMode) {
this.repeatMode = repeatMode;
internalPlayer.setRepeatMode(repeatMode);
- for (Player.EventListener listener : listeners) {
- listener.onRepeatModeChanged(repeatMode);
- }
+ notifyListeners(listener -> listener.onRepeatModeChanged(repeatMode));
}
}
@@ -267,9 +280,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
if (this.shuffleModeEnabled != shuffleModeEnabled) {
this.shuffleModeEnabled = shuffleModeEnabled;
internalPlayer.setShuffleModeEnabled(shuffleModeEnabled);
- for (Player.EventListener listener : listeners) {
- listener.onShuffleModeEnabledChanged(shuffleModeEnabled);
- }
+ notifyListeners(listener -> listener.onShuffleModeEnabledChanged(shuffleModeEnabled));
}
}
@@ -283,21 +294,6 @@ import java.util.concurrent.CopyOnWriteArraySet;
return playbackInfo.isLoading;
}
- @Override
- public void seekToDefaultPosition() {
- seekToDefaultPosition(getCurrentWindowIndex());
- }
-
- @Override
- public void seekToDefaultPosition(int windowIndex) {
- seekTo(windowIndex, C.TIME_UNSET);
- }
-
- @Override
- public void seekTo(long positionMs) {
- seekTo(getCurrentWindowIndex(), positionMs);
- }
-
@Override
public void seekTo(int windowIndex, long positionMs) {
Timeline timeline = playbackInfo.timeline;
@@ -333,9 +329,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
maskingPeriodIndex = timeline.getIndexOfPeriod(periodUidAndPosition.first);
}
internalPlayer.seekTo(timeline, windowIndex, C.msToUs(positionMs));
- for (Player.EventListener listener : listeners) {
- listener.onPositionDiscontinuity(DISCONTINUITY_REASON_SEEK);
- }
+ notifyListeners(listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_SEEK));
}
@Override
@@ -343,7 +337,14 @@ import java.util.concurrent.CopyOnWriteArraySet;
if (playbackParameters == null) {
playbackParameters = PlaybackParameters.DEFAULT;
}
+ if (this.playbackParameters.equals(playbackParameters)) {
+ return;
+ }
+ pendingSetPlaybackParametersAcks++;
+ this.playbackParameters = playbackParameters;
internalPlayer.setPlaybackParameters(playbackParameters);
+ PlaybackParameters playbackParametersToNotify = playbackParameters;
+ notifyListeners(listener -> listener.onPlaybackParametersChanged(playbackParametersToNotify));
}
@Override
@@ -368,22 +369,18 @@ import java.util.concurrent.CopyOnWriteArraySet;
}
@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);
+ public void setForegroundMode(boolean foregroundMode) {
+ if (this.foregroundMode != foregroundMode) {
+ this.foregroundMode = foregroundMode;
+ internalPlayer.setForegroundMode(foregroundMode);
+ }
}
@Override
public void stop(boolean reset) {
if (reset) {
playbackError = null;
+ mediaSource = null;
}
PlaybackInfo playbackInfo =
getResetPlaybackInfo(
@@ -401,8 +398,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
/* positionDiscontinuity= */ false,
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
TIMELINE_CHANGE_REASON_RESET,
- /* seekProcessed= */ false,
- /* playWhenReadyChanged= */ false);
+ /* seekProcessed= */ false);
}
@Override
@@ -410,17 +406,14 @@ import java.util.concurrent.CopyOnWriteArraySet;
Log.i(TAG, "Release " + Integer.toHexString(System.identityHashCode(this)) + " ["
+ ExoPlayerLibraryInfo.VERSION_SLASHY + "] [" + Util.DEVICE_DEBUG_INFO + "] ["
+ ExoPlayerLibraryInfo.registeredModules() + "]");
+ mediaSource = null;
internalPlayer.release();
eventHandler.removeCallbacksAndMessages(null);
- }
-
- @Override
- @Deprecated
- @SuppressWarnings("deprecation")
- public void sendMessages(ExoPlayerMessage... messages) {
- for (ExoPlayerMessage message : messages) {
- createMessage(message.target).setType(message.messageType).setPayload(message.message).send();
- }
+ playbackInfo =
+ getResetPlaybackInfo(
+ /* resetPosition= */ false,
+ /* resetState= */ false,
+ /* playbackState= */ Player.STATE_IDLE);
}
@Override
@@ -433,36 +426,6 @@ import java.util.concurrent.CopyOnWriteArraySet;
internalPlayerHandler);
}
- @Override
- @Deprecated
- @SuppressWarnings("deprecation")
- public void blockingSendMessages(ExoPlayerMessage... messages) {
- List playerMessages = new ArrayList<>();
- for (ExoPlayerMessage message : messages) {
- playerMessages.add(
- createMessage(message.target)
- .setType(message.messageType)
- .setPayload(message.message)
- .send());
- }
- boolean wasInterrupted = false;
- for (PlayerMessage message : playerMessages) {
- boolean blockMessage = true;
- while (blockMessage) {
- try {
- message.blockUntilDelivered();
- blockMessage = false;
- } catch (InterruptedException e) {
- wasInterrupted = true;
- }
- }
- }
- if (wasInterrupted) {
- // Restore the interrupted status.
- Thread.currentThread().interrupt();
- }
- }
-
@Override
public int getCurrentPeriodIndex() {
if (shouldMaskPosition()) {
@@ -482,20 +445,6 @@ import java.util.concurrent.CopyOnWriteArraySet;
}
}
- @Override
- public int getNextWindowIndex() {
- Timeline timeline = playbackInfo.timeline;
- return timeline.isEmpty() ? C.INDEX_UNSET
- : timeline.getNextWindowIndex(getCurrentWindowIndex(), repeatMode, shuffleModeEnabled);
- }
-
- @Override
- public int getPreviousWindowIndex() {
- Timeline timeline = playbackInfo.timeline;
- return timeline.isEmpty() ? C.INDEX_UNSET
- : timeline.getPreviousWindowIndex(getCurrentWindowIndex(), repeatMode, shuffleModeEnabled);
- }
-
@Override
public long getDuration() {
if (isPlayingAd()) {
@@ -528,30 +477,9 @@ import java.util.concurrent.CopyOnWriteArraySet;
return getContentBufferedPosition();
}
- @Override
- public int getBufferedPercentage() {
- long position = getBufferedPosition();
- long duration = getDuration();
- return position == C.TIME_UNSET || duration == C.TIME_UNSET
- ? 0
- : (duration == 0 ? 100 : Util.constrainValue((int) ((position * 100) / duration), 0, 100));
- }
-
@Override
public long getTotalBufferedDuration() {
- return Math.max(0, C.usToMs(playbackInfo.totalBufferedDurationUs));
- }
-
- @Override
- public boolean isCurrentWindowDynamic() {
- Timeline timeline = playbackInfo.timeline;
- return !timeline.isEmpty() && timeline.getWindow(getCurrentWindowIndex(), window).isDynamic;
- }
-
- @Override
- public boolean isCurrentWindowSeekable() {
- Timeline timeline = playbackInfo.timeline;
- return !timeline.isEmpty() && timeline.getWindow(getCurrentWindowIndex(), window).isSeekable;
+ return C.usToMs(playbackInfo.totalBufferedDurationUs);
}
@Override
@@ -569,18 +497,13 @@ import java.util.concurrent.CopyOnWriteArraySet;
return isPlayingAd() ? playbackInfo.periodId.adIndexInAdGroup : C.INDEX_UNSET;
}
- @Override
- public long getContentDuration() {
- return playbackInfo.timeline.isEmpty()
- ? C.TIME_UNSET
- : playbackInfo.timeline.getWindow(getCurrentWindowIndex(), window).getDurationMs();
- }
-
@Override
public long getContentPosition() {
if (isPlayingAd()) {
playbackInfo.timeline.getPeriodByUid(playbackInfo.periodId.periodUid, period);
- return period.getPositionInWindowMs() + C.usToMs(playbackInfo.contentPositionUs);
+ return playbackInfo.contentPositionUs == C.TIME_UNSET
+ ? playbackInfo.timeline.getWindow(getCurrentWindowIndex(), window).getDefaultPositionMs()
+ : period.getPositionInWindowMs() + C.usToMs(playbackInfo.contentPositionUs);
} else {
return getCurrentPosition();
}
@@ -634,11 +557,6 @@ import java.util.concurrent.CopyOnWriteArraySet;
return playbackInfo.timeline;
}
- @Override
- public Object getCurrentManifest() {
- return playbackInfo.manifest;
- }
-
// Not private so it can be called from an inner class without going through a thunk method.
/* package */ void handleEvent(Message msg) {
switch (msg.what) {
@@ -650,26 +568,31 @@ import java.util.concurrent.CopyOnWriteArraySet;
/* positionDiscontinuityReason= */ msg.arg2);
break;
case ExoPlayerImplInternal.MSG_PLAYBACK_PARAMETERS_CHANGED:
- PlaybackParameters playbackParameters = (PlaybackParameters) msg.obj;
- if (!this.playbackParameters.equals(playbackParameters)) {
- this.playbackParameters = playbackParameters;
- for (Player.EventListener listener : listeners) {
- listener.onPlaybackParametersChanged(playbackParameters);
- }
- }
+ handlePlaybackParameters((PlaybackParameters) msg.obj, /* operationAck= */ msg.arg1 != 0);
break;
case ExoPlayerImplInternal.MSG_ERROR:
ExoPlaybackException playbackError = (ExoPlaybackException) msg.obj;
this.playbackError = playbackError;
- for (Player.EventListener listener : listeners) {
- listener.onPlayerError(playbackError);
- }
+ notifyListeners(listener -> listener.onPlayerError(playbackError));
break;
default:
throw new IllegalStateException();
}
}
+ private void handlePlaybackParameters(
+ PlaybackParameters playbackParameters, boolean operationAck) {
+ if (operationAck) {
+ pendingSetPlaybackParametersAcks--;
+ }
+ if (pendingSetPlaybackParametersAcks == 0) {
+ if (!this.playbackParameters.equals(playbackParameters)) {
+ this.playbackParameters = playbackParameters;
+ notifyListeners(listener -> listener.onPlaybackParametersChanged(playbackParameters));
+ }
+ }
+ }
+
private void handlePlaybackInfo(
PlaybackInfo playbackInfo,
int operationAcks,
@@ -680,11 +603,13 @@ import java.util.concurrent.CopyOnWriteArraySet;
if (playbackInfo.startPositionUs == C.TIME_UNSET) {
// Replace internal unset start position with externally visible start position of zero.
playbackInfo =
- playbackInfo.fromNewPosition(
- playbackInfo.periodId, /* startPositionUs= */ 0, playbackInfo.contentPositionUs);
+ playbackInfo.copyWithNewPosition(
+ playbackInfo.periodId,
+ /* positionUs= */ 0,
+ playbackInfo.contentPositionUs,
+ playbackInfo.totalBufferedDurationUs);
}
- if ((!this.playbackInfo.timeline.isEmpty() || hasPendingPrepare)
- && playbackInfo.timeline.isEmpty()) {
+ if (!this.playbackInfo.timeline.isEmpty() && playbackInfo.timeline.isEmpty()) {
// Update the masking variables, which are used when the timeline becomes empty.
maskingPeriodIndex = 0;
maskingWindowIndex = 0;
@@ -703,13 +628,12 @@ import java.util.concurrent.CopyOnWriteArraySet;
positionDiscontinuity,
positionDiscontinuityReason,
timelineChangeReason,
- seekProcessed,
- /* playWhenReadyChanged= */ false);
+ seekProcessed);
}
}
private PlaybackInfo getResetPlaybackInfo(
- boolean resetPosition, boolean resetState, int playbackState) {
+ boolean resetPosition, boolean resetState, @Player.State int playbackState) {
if (resetPosition) {
maskingWindowIndex = 0;
maskingPeriodIndex = 0;
@@ -719,20 +643,27 @@ import java.util.concurrent.CopyOnWriteArraySet;
maskingPeriodIndex = getCurrentPeriodIndex();
maskingWindowPositionMs = getCurrentPosition();
}
+ // Also reset period-based PlaybackInfo positions if resetting the state.
+ resetPosition = resetPosition || resetState;
+ MediaPeriodId mediaPeriodId =
+ resetPosition
+ ? playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window, period)
+ : playbackInfo.periodId;
+ long startPositionUs = resetPosition ? 0 : playbackInfo.positionUs;
+ long contentPositionUs = resetPosition ? C.TIME_UNSET : playbackInfo.contentPositionUs;
return new PlaybackInfo(
resetState ? Timeline.EMPTY : playbackInfo.timeline,
- resetState ? null : playbackInfo.manifest,
- playbackInfo.periodId,
- playbackInfo.startPositionUs,
- playbackInfo.contentPositionUs,
+ mediaPeriodId,
+ startPositionUs,
+ contentPositionUs,
playbackState,
/* isLoading= */ false,
resetState ? TrackGroupArray.EMPTY : playbackInfo.trackGroups,
resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult,
- playbackInfo.periodId,
- playbackInfo.startPositionUs,
+ mediaPeriodId,
+ startPositionUs,
/* totalBufferedDurationUs= */ 0,
- playbackInfo.startPositionUs);
+ startPositionUs);
}
private void updatePlaybackInfo(
@@ -740,29 +671,37 @@ import java.util.concurrent.CopyOnWriteArraySet;
boolean positionDiscontinuity,
@Player.DiscontinuityReason int positionDiscontinuityReason,
@Player.TimelineChangeReason int timelineChangeReason,
- boolean seekProcessed,
- boolean playWhenReadyChanged) {
- boolean isRunningRecursiveListenerNotification = !pendingPlaybackInfoUpdates.isEmpty();
- pendingPlaybackInfoUpdates.addLast(
+ boolean seekProcessed) {
+ // Assign playback info immediately such that all getters return the right values.
+ PlaybackInfo previousPlaybackInfo = this.playbackInfo;
+ this.playbackInfo = playbackInfo;
+ notifyListeners(
new PlaybackInfoUpdate(
playbackInfo,
- /* previousPlaybackInfo= */ this.playbackInfo,
+ previousPlaybackInfo,
listeners,
trackSelector,
positionDiscontinuity,
positionDiscontinuityReason,
timelineChangeReason,
seekProcessed,
- playWhenReady,
- playWhenReadyChanged));
- // Assign playback info immediately such that all getters return the right values.
- this.playbackInfo = playbackInfo;
+ playWhenReady));
+ }
+
+ private void notifyListeners(ListenerInvocation listenerInvocation) {
+ CopyOnWriteArrayList listenerSnapshot = new CopyOnWriteArrayList<>(listeners);
+ notifyListeners(() -> invokeAll(listenerSnapshot, listenerInvocation));
+ }
+
+ private void notifyListeners(Runnable listenerNotificationRunnable) {
+ boolean isRunningRecursiveListenerNotification = !pendingListenerNotifications.isEmpty();
+ pendingListenerNotifications.addLast(listenerNotificationRunnable);
if (isRunningRecursiveListenerNotification) {
return;
}
- while (!pendingPlaybackInfoUpdates.isEmpty()) {
- pendingPlaybackInfoUpdates.peekFirst().notifyListeners();
- pendingPlaybackInfoUpdates.removeFirst();
+ while (!pendingListenerNotifications.isEmpty()) {
+ pendingListenerNotifications.peekFirst().run();
+ pendingListenerNotifications.removeFirst();
}
}
@@ -777,84 +716,84 @@ import java.util.concurrent.CopyOnWriteArraySet;
return playbackInfo.timeline.isEmpty() || pendingOperationAcks > 0;
}
- private static final class PlaybackInfoUpdate {
+ private static final class PlaybackInfoUpdate implements Runnable {
private final PlaybackInfo playbackInfo;
- private final Set listeners;
+ private final CopyOnWriteArrayList listenerSnapshot;
private final TrackSelector trackSelector;
private final boolean positionDiscontinuity;
private final @Player.DiscontinuityReason int positionDiscontinuityReason;
private final @Player.TimelineChangeReason int timelineChangeReason;
private final boolean seekProcessed;
- private final boolean playWhenReady;
- private final boolean playbackStateOrPlayWhenReadyChanged;
- private final boolean timelineOrManifestChanged;
+ private final boolean playbackStateChanged;
+ private final boolean timelineChanged;
private final boolean isLoadingChanged;
private final boolean trackSelectorResultChanged;
+ private final boolean playWhenReady;
public PlaybackInfoUpdate(
PlaybackInfo playbackInfo,
PlaybackInfo previousPlaybackInfo,
- Set listeners,
+ CopyOnWriteArrayList listeners,
TrackSelector trackSelector,
boolean positionDiscontinuity,
@Player.DiscontinuityReason int positionDiscontinuityReason,
@Player.TimelineChangeReason int timelineChangeReason,
boolean seekProcessed,
- boolean playWhenReady,
- boolean playWhenReadyChanged) {
+ boolean playWhenReady) {
this.playbackInfo = playbackInfo;
- this.listeners = listeners;
+ this.listenerSnapshot = new CopyOnWriteArrayList<>(listeners);
this.trackSelector = trackSelector;
this.positionDiscontinuity = positionDiscontinuity;
this.positionDiscontinuityReason = positionDiscontinuityReason;
this.timelineChangeReason = timelineChangeReason;
this.seekProcessed = seekProcessed;
this.playWhenReady = playWhenReady;
- playbackStateOrPlayWhenReadyChanged =
- playWhenReadyChanged || previousPlaybackInfo.playbackState != playbackInfo.playbackState;
- timelineOrManifestChanged =
- previousPlaybackInfo.timeline != playbackInfo.timeline
- || previousPlaybackInfo.manifest != playbackInfo.manifest;
+ playbackStateChanged = previousPlaybackInfo.playbackState != playbackInfo.playbackState;
+ timelineChanged = previousPlaybackInfo.timeline != playbackInfo.timeline;
isLoadingChanged = previousPlaybackInfo.isLoading != playbackInfo.isLoading;
trackSelectorResultChanged =
previousPlaybackInfo.trackSelectorResult != playbackInfo.trackSelectorResult;
}
- public void notifyListeners() {
- if (timelineOrManifestChanged || timelineChangeReason == TIMELINE_CHANGE_REASON_PREPARED) {
- for (Player.EventListener listener : listeners) {
- listener.onTimelineChanged(
- playbackInfo.timeline, playbackInfo.manifest, timelineChangeReason);
- }
+ @Override
+ public void run() {
+ if (timelineChanged || timelineChangeReason == TIMELINE_CHANGE_REASON_PREPARED) {
+ invokeAll(
+ listenerSnapshot,
+ listener -> listener.onTimelineChanged(playbackInfo.timeline, timelineChangeReason));
}
if (positionDiscontinuity) {
- for (Player.EventListener listener : listeners) {
- listener.onPositionDiscontinuity(positionDiscontinuityReason);
- }
+ invokeAll(
+ listenerSnapshot,
+ listener -> listener.onPositionDiscontinuity(positionDiscontinuityReason));
}
if (trackSelectorResultChanged) {
trackSelector.onSelectionActivated(playbackInfo.trackSelectorResult.info);
- for (Player.EventListener listener : listeners) {
- listener.onTracksChanged(
- playbackInfo.trackGroups, playbackInfo.trackSelectorResult.selections);
- }
+ invokeAll(
+ listenerSnapshot,
+ listener ->
+ listener.onTracksChanged(
+ playbackInfo.trackGroups, playbackInfo.trackSelectorResult.selections));
}
if (isLoadingChanged) {
- for (Player.EventListener listener : listeners) {
- listener.onLoadingChanged(playbackInfo.isLoading);
- }
+ invokeAll(listenerSnapshot, listener -> listener.onLoadingChanged(playbackInfo.isLoading));
}
- if (playbackStateOrPlayWhenReadyChanged) {
- for (Player.EventListener listener : listeners) {
- listener.onPlayerStateChanged(playWhenReady, playbackInfo.playbackState);
- }
+ if (playbackStateChanged) {
+ invokeAll(
+ listenerSnapshot,
+ listener -> listener.onPlayerStateChanged(playWhenReady, playbackInfo.playbackState));
}
if (seekProcessed) {
- for (Player.EventListener listener : listeners) {
- listener.onSeekProcessed();
- }
+ invokeAll(listenerSnapshot, EventListener::onSeekProcessed);
}
}
}
+
+ private static void invokeAll(
+ CopyOnWriteArrayList listeners, ListenerInvocation listenerInvocation) {
+ for (ListenerHolder listenerHolder : listeners) {
+ listenerHolder.invoke(listenerInvocation);
+ }
+ }
}
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 f2aeb7321d..6ab0838e26 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
@@ -21,15 +21,15 @@ import android.os.Looper;
import android.os.Message;
import android.os.Process;
import android.os.SystemClock;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.util.Log;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import android.util.Pair;
import com.google.android.exoplayer2.DefaultMediaClock.PlaybackParameterListener;
import com.google.android.exoplayer2.Player.DiscontinuityReason;
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.MediaSource.MediaSourceCaller;
import com.google.android.exoplayer2.source.SampleStream;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.TrackSelection;
@@ -39,18 +39,20 @@ 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.util.HandlerWrapper;
+import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.TraceUtil;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.concurrent.atomic.AtomicBoolean;
/** Implements the internal behavior of {@link ExoPlayerImpl}. */
/* package */ final class ExoPlayerImplInternal
implements Handler.Callback,
MediaPeriod.Callback,
TrackSelector.InvalidationListener,
- MediaSource.SourceInfoRefreshListener,
+ MediaSourceCaller,
PlaybackParameterListener,
PlayerMessage.Sender {
@@ -76,9 +78,10 @@ import java.util.Collections;
private static final int MSG_TRACK_SELECTION_INVALIDATED = 11;
private static final int MSG_SET_REPEAT_MODE = 12;
private static final int MSG_SET_SHUFFLE_ENABLED = 13;
- private static final int MSG_SEND_MESSAGE = 14;
- private static final int MSG_SEND_MESSAGE_TO_TARGET_THREAD = 15;
- private static final int MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL = 16;
+ private static final int MSG_SET_FOREGROUND_MODE = 14;
+ private static final int MSG_SEND_MESSAGE = 15;
+ private static final int MSG_SEND_MESSAGE_TO_TARGET_THREAD = 16;
+ private static final int MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL = 17;
private static final int PREPARING_SOURCE_INTERVAL_MS = 10;
private static final int RENDERING_INTERVAL_MS = 10;
@@ -93,7 +96,6 @@ import java.util.Collections;
private final HandlerWrapper handler;
private final HandlerThread internalPlaybackThread;
private final Handler eventHandler;
- private final ExoPlayer player;
private final Timeline.Window window;
private final Timeline.Period period;
private final long backBufferDurationUs;
@@ -115,6 +117,7 @@ import java.util.Collections;
private boolean rebuffering;
@Player.RepeatMode private int repeatMode;
private boolean shuffleModeEnabled;
+ private boolean foregroundMode;
private int pendingPrepareCount;
private SeekPosition pendingInitialSeekPosition;
@@ -131,7 +134,6 @@ import java.util.Collections;
@Player.RepeatMode int repeatMode,
boolean shuffleModeEnabled,
Handler eventHandler,
- ExoPlayer player,
Clock clock) {
this.renderers = renderers;
this.trackSelector = trackSelector;
@@ -142,7 +144,6 @@ import java.util.Collections;
this.repeatMode = repeatMode;
this.shuffleModeEnabled = shuffleModeEnabled;
this.eventHandler = eventHandler;
- this.player = player;
this.clock = clock;
this.queue = new MediaPeriodQueue();
@@ -218,6 +219,29 @@ import java.util.Collections;
handler.obtainMessage(MSG_SEND_MESSAGE, message).sendToTarget();
}
+ public synchronized void setForegroundMode(boolean foregroundMode) {
+ if (foregroundMode) {
+ handler.obtainMessage(MSG_SET_FOREGROUND_MODE, /* foregroundMode */ 1, 0).sendToTarget();
+ } else {
+ AtomicBoolean processedFlag = new AtomicBoolean();
+ handler
+ .obtainMessage(MSG_SET_FOREGROUND_MODE, /* foregroundMode */ 0, 0, processedFlag)
+ .sendToTarget();
+ boolean wasInterrupted = false;
+ while (!processedFlag.get() && !released) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ wasInterrupted = true;
+ }
+ }
+ if (wasInterrupted) {
+ // Restore the interrupted status.
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+
public synchronized void release() {
if (released) {
return;
@@ -241,12 +265,13 @@ import java.util.Collections;
return internalPlaybackThread.getLooper();
}
- // MediaSource.SourceInfoRefreshListener implementation.
+ // MediaSource.MediaSourceCaller implementation.
@Override
- public void onSourceInfoRefreshed(MediaSource source, Timeline timeline, Object manifest) {
- handler.obtainMessage(MSG_REFRESH_SOURCE_INFO,
- new MediaSourceRefreshInfo(source, timeline, manifest)).sendToTarget();
+ public void onSourceInfoRefreshed(MediaSource source, Timeline timeline) {
+ handler
+ .obtainMessage(MSG_REFRESH_SOURCE_INFO, new MediaSourceRefreshInfo(source, timeline))
+ .sendToTarget();
}
// MediaPeriod.Callback implementation.
@@ -272,14 +297,11 @@ import java.util.Collections;
@Override
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
- handler
- .obtainMessage(MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL, playbackParameters)
- .sendToTarget();
+ sendPlaybackParametersChangedInternal(playbackParameters, /* acknowledgeCommand= */ false);
}
// Handler.Callback implementation.
- @SuppressWarnings("unchecked")
@Override
public boolean handleMessage(Message msg) {
try {
@@ -311,8 +333,15 @@ import java.util.Collections;
case MSG_SET_SEEK_PARAMETERS:
setSeekParametersInternal((SeekParameters) msg.obj);
break;
+ case MSG_SET_FOREGROUND_MODE:
+ setForegroundModeInternal(
+ /* foregroundMode= */ msg.arg1 != 0, /* processedFlag= */ (AtomicBoolean) msg.obj);
+ break;
case MSG_STOP:
- stopInternal(/* reset= */ msg.arg1 != 0, /* acknowledgeStop= */ true);
+ stopInternal(
+ /* forceResetRenderers= */ false,
+ /* resetPositionAndState= */ msg.arg1 != 0,
+ /* acknowledgeStop= */ true);
break;
case MSG_PERIOD_PREPARED:
handlePeriodPrepared((MediaPeriod) msg.obj);
@@ -327,7 +356,8 @@ import java.util.Collections;
reselectTracksInternal();
break;
case MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL:
- handlePlaybackParameters((PlaybackParameters) msg.obj);
+ handlePlaybackParameters(
+ (PlaybackParameters) msg.obj, /* acknowledgeCommand= */ msg.arg1 != 0);
break;
case MSG_SEND_MESSAGE:
sendMessageInternal((PlayerMessage) msg.obj);
@@ -345,19 +375,31 @@ import java.util.Collections;
maybeNotifyPlaybackInfoChanged();
} catch (ExoPlaybackException e) {
Log.e(TAG, "Playback error.", e);
- stopInternal(/* reset= */ false, /* acknowledgeStop= */ false);
eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget();
+ stopInternal(
+ /* forceResetRenderers= */ true,
+ /* resetPositionAndState= */ false,
+ /* acknowledgeStop= */ false);
maybeNotifyPlaybackInfoChanged();
} catch (IOException e) {
Log.e(TAG, "Source error.", e);
- stopInternal(/* reset= */ false, /* acknowledgeStop= */ false);
eventHandler.obtainMessage(MSG_ERROR, ExoPlaybackException.createForSource(e)).sendToTarget();
+ stopInternal(
+ /* forceResetRenderers= */ false,
+ /* resetPositionAndState= */ false,
+ /* acknowledgeStop= */ false);
maybeNotifyPlaybackInfoChanged();
- } catch (RuntimeException e) {
+ } catch (RuntimeException | OutOfMemoryError e) {
Log.e(TAG, "Internal runtime error.", e);
- stopInternal(/* reset= */ false, /* acknowledgeStop= */ false);
- eventHandler.obtainMessage(MSG_ERROR, ExoPlaybackException.createForUnexpected(e))
- .sendToTarget();
+ ExoPlaybackException error =
+ e instanceof OutOfMemoryError
+ ? ExoPlaybackException.createForOutOfMemoryError((OutOfMemoryError) e)
+ : ExoPlaybackException.createForUnexpected((RuntimeException) e);
+ eventHandler.obtainMessage(MSG_ERROR, error).sendToTarget();
+ stopInternal(
+ /* forceResetRenderers= */ true,
+ /* resetPositionAndState= */ false,
+ /* acknowledgeStop= */ false);
maybeNotifyPlaybackInfoChanged();
}
return true;
@@ -394,15 +436,12 @@ import java.util.Collections;
private void prepareInternal(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
pendingPrepareCount++;
- resetInternal(/* releaseMediaSource= */ true, resetPosition, resetState);
+ resetInternal(
+ /* resetRenderers= */ false, /* releaseMediaSource= */ true, resetPosition, resetState);
loadControl.onPrepared();
this.mediaSource = mediaSource;
setState(Player.STATE_BUFFERING);
- mediaSource.prepareSource(
- player,
- /* isTopLevelSource= */ true,
- /* listener= */ this,
- bandwidthMeter.getTransferListener());
+ mediaSource.prepareSource(/* caller= */ this, bandwidthMeter.getTransferListener());
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
}
@@ -428,7 +467,7 @@ import java.util.Collections;
if (!queue.updateRepeatMode(repeatMode)) {
seekToCurrentPosition(/* sendDiscontinuity= */ true);
}
- updateLoadingMediaPeriodId();
+ handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false);
}
private void setShuffleModeEnabledInternal(boolean shuffleModeEnabled)
@@ -437,7 +476,7 @@ import java.util.Collections;
if (!queue.updateShuffleModeEnabled(shuffleModeEnabled)) {
seekToCurrentPosition(/* sendDiscontinuity= */ true);
}
- updateLoadingMediaPeriodId();
+ handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false);
}
private void seekToCurrentPosition(boolean sendDiscontinuity) throws ExoPlaybackException {
@@ -448,7 +487,11 @@ import java.util.Collections;
seekToPeriodPosition(periodId, playbackInfo.positionUs, /* forceDisableRenderers= */ true);
if (newPositionUs != playbackInfo.positionUs) {
playbackInfo =
- playbackInfo.fromNewPosition(periodId, newPositionUs, playbackInfo.contentPositionUs);
+ playbackInfo.copyWithNewPosition(
+ periodId,
+ newPositionUs,
+ playbackInfo.contentPositionUs,
+ getTotalBufferedDurationUs());
if (sendDiscontinuity) {
playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL);
}
@@ -483,12 +526,18 @@ import java.util.Collections;
// A MediaPeriod may report a discontinuity at the current playback position to ensure the
// renderers are flushed. Only report the discontinuity externally if the position changed.
if (periodPositionUs != playbackInfo.positionUs) {
- playbackInfo = playbackInfo.fromNewPosition(playbackInfo.periodId, periodPositionUs,
- playbackInfo.contentPositionUs);
+ playbackInfo =
+ playbackInfo.copyWithNewPosition(
+ playbackInfo.periodId,
+ periodPositionUs,
+ playbackInfo.contentPositionUs,
+ getTotalBufferedDurationUs());
playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL);
}
} else {
- rendererPositionUs = mediaClock.syncAndGetPositionUs();
+ rendererPositionUs =
+ mediaClock.syncAndGetPositionUs(
+ /* isReadingAhead= */ playingPeriodHolder != queue.getReadingPeriod());
periodPositionUs = playingPeriodHolder.toPeriodTime(rendererPositionUs);
maybeTriggerPendingMessages(playbackInfo.positionUs, periodPositionUs);
playbackInfo.positionUs = periodPositionUs;
@@ -496,10 +545,8 @@ import java.util.Collections;
// Update the buffered position and total buffered duration.
MediaPeriodHolder loadingPeriod = queue.getLoadingPeriod();
- playbackInfo.bufferedPositionUs =
- loadingPeriod.getBufferedPositionUs(/* convertEosToDuration= */ true);
- playbackInfo.totalBufferedDurationUs =
- playbackInfo.bufferedPositionUs - loadingPeriod.toPeriodTime(rendererPositionUs);
+ playbackInfo.bufferedPositionUs = loadingPeriod.getBufferedPositionUs();
+ playbackInfo.totalBufferedDurationUs = getTotalBufferedDurationUs();
}
private void doSomeWork() throws ExoPlaybackException, IOException {
@@ -599,7 +646,7 @@ import java.util.Collections;
if (resolvedSeekPosition == null) {
// The seek position was valid for the timeline that it was performed into, but the
// timeline has changed or is not ready and a suitable seek position could not be resolved.
- periodId = getFirstMediaPeriodId();
+ periodId = playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window, period);
periodPositionUs = C.TIME_UNSET;
contentPositionUs = C.TIME_UNSET;
seekPositionAdjusted = true;
@@ -625,7 +672,10 @@ import java.util.Collections;
// End playback, as we didn't manage to find a valid seek position.
setState(Player.STATE_ENDED);
resetInternal(
- /* releaseMediaSource= */ false, /* resetPosition= */ true, /* resetState= */ false);
+ /* resetRenderers= */ false,
+ /* releaseMediaSource= */ false,
+ /* resetPosition= */ true,
+ /* resetState= */ false);
} else {
// Execute the seek in the current media periods.
long newPeriodPositionUs = periodPositionUs;
@@ -647,7 +697,9 @@ import java.util.Collections;
periodPositionUs = newPeriodPositionUs;
}
} finally {
- playbackInfo = playbackInfo.fromNewPosition(periodId, periodPositionUs, contentPositionUs);
+ playbackInfo =
+ playbackInfo.copyWithNewPosition(
+ periodId, periodPositionUs, contentPositionUs, getTotalBufferedDurationUs());
if (seekPositionAdjusted) {
playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT);
}
@@ -679,13 +731,20 @@ import java.util.Collections;
newPlayingPeriodHolder = queue.advancePlayingPeriod();
}
- // Disable all the renderers if the period being played is changing, or if forced.
- if (oldPlayingPeriodHolder != newPlayingPeriodHolder || forceDisableRenderers) {
+ // Disable all renderers if the period being played is changing, if the seek results in negative
+ // renderer timestamps, or if forced.
+ if (forceDisableRenderers
+ || oldPlayingPeriodHolder != newPlayingPeriodHolder
+ || (newPlayingPeriodHolder != null
+ && newPlayingPeriodHolder.toRendererTime(periodPositionUs) < 0)) {
for (Renderer renderer : enabledRenderers) {
disableRenderer(renderer);
}
enabledRenderers = new Renderer[0];
oldPlayingPeriodHolder = null;
+ if (newPlayingPeriodHolder != null) {
+ newPlayingPeriodHolder.setRendererOffset(/* rendererPositionOffsetUs= */ 0);
+ }
}
// Update the holders.
@@ -706,7 +765,7 @@ import java.util.Collections;
resetRendererPosition(periodPositionUs);
}
- updateLoadingMediaPeriodId();
+ handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false);
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
return periodPositionUs;
}
@@ -720,19 +779,46 @@ import java.util.Collections;
for (Renderer renderer : enabledRenderers) {
renderer.resetPosition(rendererPositionUs);
}
+ notifyTrackSelectionDiscontinuity();
}
private void setPlaybackParametersInternal(PlaybackParameters playbackParameters) {
mediaClock.setPlaybackParameters(playbackParameters);
+ sendPlaybackParametersChangedInternal(
+ mediaClock.getPlaybackParameters(), /* acknowledgeCommand= */ true);
}
private void setSeekParametersInternal(SeekParameters seekParameters) {
this.seekParameters = seekParameters;
}
- private void stopInternal(boolean reset, boolean acknowledgeStop) {
+ private void setForegroundModeInternal(
+ boolean foregroundMode, @Nullable AtomicBoolean processedFlag) {
+ if (this.foregroundMode != foregroundMode) {
+ this.foregroundMode = foregroundMode;
+ if (!foregroundMode) {
+ for (Renderer renderer : renderers) {
+ if (renderer.getState() == Renderer.STATE_DISABLED) {
+ renderer.reset();
+ }
+ }
+ }
+ }
+ if (processedFlag != null) {
+ synchronized (this) {
+ processedFlag.set(true);
+ notifyAll();
+ }
+ }
+ }
+
+ private void stopInternal(
+ boolean forceResetRenderers, boolean resetPositionAndState, boolean acknowledgeStop) {
resetInternal(
- /* releaseMediaSource= */ true, /* resetPosition= */ reset, /* resetState= */ reset);
+ /* resetRenderers= */ forceResetRenderers || !foregroundMode,
+ /* releaseMediaSource= */ true,
+ /* resetPosition= */ resetPositionAndState,
+ /* resetState= */ resetPositionAndState);
playbackInfoUpdate.incrementPendingOperationAcks(
pendingPrepareCount + (acknowledgeStop ? 1 : 0));
pendingPrepareCount = 0;
@@ -742,7 +828,10 @@ import java.util.Collections;
private void releaseInternal() {
resetInternal(
- /* releaseMediaSource= */ true, /* resetPosition= */ true, /* resetState= */ true);
+ /* resetRenderers= */ true,
+ /* releaseMediaSource= */ true,
+ /* resetPosition= */ true,
+ /* resetState= */ true);
loadControl.onReleased();
setState(Player.STATE_IDLE);
internalPlaybackThread.quit();
@@ -752,19 +841,11 @@ import java.util.Collections;
}
}
- private MediaPeriodId getFirstMediaPeriodId() {
- Timeline timeline = playbackInfo.timeline;
- if (timeline.isEmpty()) {
- return PlaybackInfo.DUMMY_MEDIA_PERIOD_ID;
- }
- int firstPeriodIndex =
- timeline.getWindow(timeline.getFirstWindowIndex(shuffleModeEnabled), window)
- .firstPeriodIndex;
- return new MediaPeriodId(timeline.getUidOfPeriod(firstPeriodIndex));
- }
-
private void resetInternal(
- boolean releaseMediaSource, boolean resetPosition, boolean resetState) {
+ boolean resetRenderers,
+ boolean releaseMediaSource,
+ boolean resetPosition,
+ boolean resetState) {
handler.removeMessages(MSG_DO_SOME_WORK);
rebuffering = false;
mediaClock.stop();
@@ -774,15 +855,37 @@ import java.util.Collections;
disableRenderer(renderer);
} catch (ExoPlaybackException | RuntimeException e) {
// There's nothing we can do.
- Log.e(TAG, "Stop failed.", e);
+ Log.e(TAG, "Disable failed.", e);
+ }
+ }
+ if (resetRenderers) {
+ for (Renderer renderer : renderers) {
+ try {
+ renderer.reset();
+ } catch (RuntimeException e) {
+ // There's nothing we can do.
+ Log.e(TAG, "Reset failed.", e);
+ }
}
}
enabledRenderers = new Renderer[0];
- queue.clear(/* keepFrontPeriodUid= */ !resetPosition);
- setIsLoading(false);
+
if (resetPosition) {
pendingInitialSeekPosition = null;
+ } else if (resetState) {
+ // When resetting the state, also reset the period-based PlaybackInfo position and convert
+ // existing position to initial seek instead.
+ resetPosition = true;
+ if (pendingInitialSeekPosition == null && !playbackInfo.timeline.isEmpty()) {
+ playbackInfo.timeline.getPeriodByUid(playbackInfo.periodId.periodUid, period);
+ long windowPositionUs = playbackInfo.positionUs + period.getPositionInWindowUs();
+ pendingInitialSeekPosition =
+ new SeekPosition(Timeline.EMPTY, period.windowIndex, windowPositionUs);
+ }
}
+
+ queue.clear(/* keepFrontPeriodUid= */ !resetState);
+ setIsLoading(false);
if (resetState) {
queue.setTimeline(Timeline.EMPTY);
for (PendingMessageInfo pendingMessageInfo : pendingMessages) {
@@ -791,14 +894,16 @@ import java.util.Collections;
pendingMessages.clear();
nextPendingMessageIndex = 0;
}
+ MediaPeriodId mediaPeriodId =
+ resetPosition
+ ? playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window, period)
+ : playbackInfo.periodId;
// Set the start position to TIME_UNSET so that a subsequent seek to 0 isn't ignored.
- MediaPeriodId mediaPeriodId = resetPosition ? getFirstMediaPeriodId() : playbackInfo.periodId;
long startPositionUs = resetPosition ? C.TIME_UNSET : playbackInfo.positionUs;
long contentPositionUs = resetPosition ? C.TIME_UNSET : playbackInfo.contentPositionUs;
playbackInfo =
new PlaybackInfo(
resetState ? Timeline.EMPTY : playbackInfo.timeline,
- resetState ? null : playbackInfo.manifest,
mediaPeriodId,
startPositionUs,
contentPositionUs,
@@ -812,7 +917,7 @@ import java.util.Collections;
startPositionUs);
if (releaseMediaSource) {
if (mediaSource != null) {
- mediaSource.releaseSource(/* listener= */ this);
+ mediaSource.releaseSource(/* caller= */ this);
mediaSource = null;
}
}
@@ -958,11 +1063,14 @@ import java.util.Collections;
&& nextInfo.resolvedPeriodIndex == currentPeriodIndex
&& nextInfo.resolvedPeriodTimeUs > oldPeriodPositionUs
&& nextInfo.resolvedPeriodTimeUs <= newPeriodPositionUs) {
- sendMessageToTarget(nextInfo.message);
- if (nextInfo.message.getDeleteAfterDelivery() || nextInfo.message.isCanceled()) {
- pendingMessages.remove(nextPendingMessageIndex);
- } else {
- nextPendingMessageIndex++;
+ try {
+ sendMessageToTarget(nextInfo.message);
+ } finally {
+ if (nextInfo.message.getDeleteAfterDelivery() || nextInfo.message.isCanceled()) {
+ pendingMessages.remove(nextPendingMessageIndex);
+ } else {
+ nextPendingMessageIndex++;
+ }
}
nextInfo =
nextPendingMessageIndex < pendingMessages.size()
@@ -993,12 +1101,14 @@ import java.util.Collections;
MediaPeriodHolder periodHolder = queue.getPlayingPeriod();
MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod();
boolean selectionsChangedForReadPeriod = true;
+ TrackSelectorResult newTrackSelectorResult;
while (true) {
if (periodHolder == null || !periodHolder.prepared) {
// The reselection did not change any prepared periods.
return;
}
- if (periodHolder.selectTracks(playbackSpeed)) {
+ newTrackSelectorResult = periodHolder.selectTracks(playbackSpeed, playbackInfo.timeline);
+ if (!newTrackSelectorResult.isEquivalent(periodHolder.getTrackSelectorResult())) {
// Selected tracks have changed for this period.
break;
}
@@ -1006,7 +1116,7 @@ import java.util.Collections;
// The track reselection didn't affect any period that has been read.
selectionsChangedForReadPeriod = false;
}
- periodHolder = periodHolder.next;
+ periodHolder = periodHolder.getNext();
}
if (selectionsChangedForReadPeriod) {
@@ -1017,13 +1127,15 @@ import java.util.Collections;
boolean[] streamResetFlags = new boolean[renderers.length];
long periodPositionUs =
playingPeriodHolder.applyTrackSelection(
- playbackInfo.positionUs, recreateStreams, streamResetFlags);
- updateLoadControlTrackSelection(
- playingPeriodHolder.trackGroups, playingPeriodHolder.trackSelectorResult);
+ newTrackSelectorResult, playbackInfo.positionUs, recreateStreams, streamResetFlags);
if (playbackInfo.playbackState != Player.STATE_ENDED
&& periodPositionUs != playbackInfo.positionUs) {
- playbackInfo = playbackInfo.fromNewPosition(playbackInfo.periodId, periodPositionUs,
- playbackInfo.contentPositionUs);
+ playbackInfo =
+ playbackInfo.copyWithNewPosition(
+ playbackInfo.periodId,
+ periodPositionUs,
+ playbackInfo.contentPositionUs,
+ getTotalBufferedDurationUs());
playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL);
resetRendererPosition(periodPositionUs);
}
@@ -1049,7 +1161,7 @@ import java.util.Collections;
}
playbackInfo =
playbackInfo.copyWithTrackInfo(
- playingPeriodHolder.trackGroups, playingPeriodHolder.trackSelectorResult);
+ playingPeriodHolder.getTrackGroups(), playingPeriodHolder.getTrackSelectorResult());
enableRenderers(rendererWasEnabledFlags, enabledRendererCount);
} else {
// Release and re-prepare/buffer periods after the one whose selection changed.
@@ -1058,11 +1170,10 @@ import java.util.Collections;
long loadingPeriodPositionUs =
Math.max(
periodHolder.info.startPositionUs, periodHolder.toPeriodTime(rendererPositionUs));
- periodHolder.applyTrackSelection(loadingPeriodPositionUs, false);
- updateLoadControlTrackSelection(periodHolder.trackGroups, periodHolder.trackSelectorResult);
+ periodHolder.applyTrackSelection(newTrackSelectorResult, loadingPeriodPositionUs, false);
}
}
- updateLoadingMediaPeriodId();
+ handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ true);
if (playbackInfo.playbackState != Player.STATE_ENDED) {
maybeContinueLoading();
updatePlaybackPositions();
@@ -1070,23 +1181,29 @@ import java.util.Collections;
}
}
- private void updateLoadControlTrackSelection(
- TrackGroupArray trackGroups, TrackSelectorResult trackSelectorResult) {
- loadControl.onTracksSelected(renderers, trackGroups, trackSelectorResult.selections);
- }
-
private void updateTrackSelectionPlaybackSpeed(float playbackSpeed) {
MediaPeriodHolder periodHolder = queue.getFrontPeriod();
- while (periodHolder != null) {
- if (periodHolder.trackSelectorResult != null) {
- TrackSelection[] trackSelections = periodHolder.trackSelectorResult.selections.getAll();
- for (TrackSelection trackSelection : trackSelections) {
- if (trackSelection != null) {
- trackSelection.onPlaybackSpeed(playbackSpeed);
- }
+ while (periodHolder != null && periodHolder.prepared) {
+ TrackSelection[] trackSelections = periodHolder.getTrackSelectorResult().selections.getAll();
+ for (TrackSelection trackSelection : trackSelections) {
+ if (trackSelection != null) {
+ trackSelection.onPlaybackSpeed(playbackSpeed);
}
}
- periodHolder = periodHolder.next;
+ periodHolder = periodHolder.getNext();
+ }
+ }
+
+ private void notifyTrackSelectionDiscontinuity() {
+ MediaPeriodHolder periodHolder = queue.getFrontPeriod();
+ while (periodHolder != null) {
+ TrackSelection[] trackSelections = periodHolder.getTrackSelectorResult().selections.getAll();
+ for (TrackSelection trackSelection : trackSelections) {
+ if (trackSelection != null) {
+ trackSelection.onDiscontinuity();
+ }
+ }
+ periodHolder = periodHolder.getNext();
}
}
@@ -1105,28 +1222,41 @@ import java.util.Collections;
}
// Renderers are ready and we're loading. Ask the LoadControl whether to transition.
MediaPeriodHolder loadingHolder = queue.getLoadingPeriod();
- long bufferedPositionUs = loadingHolder.getBufferedPositionUs(!loadingHolder.info.isFinal);
- return bufferedPositionUs == C.TIME_END_OF_SOURCE
+ boolean bufferedToEnd = loadingHolder.isFullyBuffered() && loadingHolder.info.isFinal;
+ return bufferedToEnd
|| loadControl.shouldStartPlayback(
- bufferedPositionUs - loadingHolder.toPeriodTime(rendererPositionUs),
- mediaClock.getPlaybackParameters().speed,
- rebuffering);
+ getTotalBufferedDurationUs(), mediaClock.getPlaybackParameters().speed, rebuffering);
}
private boolean isTimelineReady() {
MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();
+ MediaPeriodHolder nextPeriodHolder = playingPeriodHolder.getNext();
long playingPeriodDurationUs = playingPeriodHolder.info.durationUs;
return playingPeriodDurationUs == C.TIME_UNSET
|| playbackInfo.positionUs < playingPeriodDurationUs
- || (playingPeriodHolder.next != null
- && (playingPeriodHolder.next.prepared || playingPeriodHolder.next.info.id.isAd()));
+ || (nextPeriodHolder != null
+ && (nextPeriodHolder.prepared || nextPeriodHolder.info.id.isAd()));
+ }
+
+ private void maybeThrowSourceInfoRefreshError() throws IOException {
+ MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod();
+ if (loadingPeriodHolder != null) {
+ // Defer throwing until we read all available media periods.
+ for (Renderer renderer : enabledRenderers) {
+ if (!renderer.hasReadStreamToEnd()) {
+ return;
+ }
+ }
+ }
+ mediaSource.maybeThrowSourceInfoRefreshError();
}
private void maybeThrowPeriodPrepareError() throws IOException {
MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod();
MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod();
- if (loadingPeriodHolder != null && !loadingPeriodHolder.prepared
- && (readingPeriodHolder == null || readingPeriodHolder.next == loadingPeriodHolder)) {
+ if (loadingPeriodHolder != null
+ && !loadingPeriodHolder.prepared
+ && (readingPeriodHolder == null || readingPeriodHolder.getNext() == loadingPeriodHolder)) {
for (Renderer renderer : enabledRenderers) {
if (!renderer.hasReadStreamToEnd()) {
return;
@@ -1142,137 +1272,119 @@ import java.util.Collections;
// Stale event.
return;
}
+ playbackInfoUpdate.incrementPendingOperationAcks(pendingPrepareCount);
+ pendingPrepareCount = 0;
Timeline oldTimeline = playbackInfo.timeline;
Timeline timeline = sourceRefreshInfo.timeline;
- Object manifest = sourceRefreshInfo.manifest;
queue.setTimeline(timeline);
- playbackInfo = playbackInfo.copyWithTimeline(timeline, manifest);
+ playbackInfo = playbackInfo.copyWithTimeline(timeline);
resolvePendingMessagePositions();
- if (pendingPrepareCount > 0) {
- playbackInfoUpdate.incrementPendingOperationAcks(pendingPrepareCount);
- pendingPrepareCount = 0;
- if (pendingInitialSeekPosition != null) {
- Pair periodPosition;
- try {
- periodPosition =
- resolveSeekPosition(pendingInitialSeekPosition, /* trySubsequentPeriods= */ true);
- } catch (IllegalSeekPositionException e) {
- playbackInfo =
- playbackInfo.fromNewPosition(getFirstMediaPeriodId(), C.TIME_UNSET, C.TIME_UNSET);
- throw e;
- }
- pendingInitialSeekPosition = null;
- if (periodPosition == null) {
- // The seek position was valid for the timeline that it was performed into, but the
- // timeline has changed and a suitable seek position could not be resolved in the new one.
- handleSourceInfoRefreshEndedPlayback();
- } else {
- Object periodUid = periodPosition.first;
- long positionUs = periodPosition.second;
- MediaPeriodId periodId = queue.resolveMediaPeriodIdForAds(periodUid, positionUs);
- playbackInfo =
- playbackInfo.fromNewPosition(
- periodId, periodId.isAd() ? 0 : positionUs, /* contentPositionUs= */ positionUs);
- }
- } else if (playbackInfo.startPositionUs == C.TIME_UNSET) {
- if (timeline.isEmpty()) {
- handleSourceInfoRefreshEndedPlayback();
- } else {
- Pair defaultPosition =
- getPeriodPosition(
- timeline, timeline.getFirstWindowIndex(shuffleModeEnabled), C.TIME_UNSET);
- Object periodUid = defaultPosition.first;
- long startPositionUs = defaultPosition.second;
- MediaPeriodId periodId = queue.resolveMediaPeriodIdForAds(periodUid, startPositionUs);
- playbackInfo =
- playbackInfo.fromNewPosition(
- periodId,
- periodId.isAd() ? 0 : startPositionUs,
- /* contentPositionUs= */ startPositionUs);
- }
+ MediaPeriodId newPeriodId = playbackInfo.periodId;
+ long oldContentPositionUs =
+ playbackInfo.periodId.isAd() ? playbackInfo.contentPositionUs : playbackInfo.positionUs;
+ long newContentPositionUs = oldContentPositionUs;
+ if (pendingInitialSeekPosition != null) {
+ // Resolve initial seek position.
+ Pair periodPosition =
+ resolveSeekPosition(pendingInitialSeekPosition, /* trySubsequentPeriods= */ true);
+ pendingInitialSeekPosition = null;
+ if (periodPosition == null) {
+ // The seek position was valid for the timeline that it was performed into, but the
+ // timeline has changed and a suitable seek position could not be resolved in the new one.
+ handleSourceInfoRefreshEndedPlayback();
+ return;
}
- return;
- }
-
- if (oldTimeline.isEmpty()) {
- // If the old timeline is empty, the period queue is also empty.
- if (!timeline.isEmpty()) {
- Pair defaultPosition =
- getPeriodPosition(
- timeline, timeline.getFirstWindowIndex(shuffleModeEnabled), C.TIME_UNSET);
- Object periodUid = defaultPosition.first;
- long startPositionUs = defaultPosition.second;
- MediaPeriodId periodId = queue.resolveMediaPeriodIdForAds(periodUid, startPositionUs);
- playbackInfo =
- playbackInfo.fromNewPosition(
- periodId,
- /* startPositionUs= */ periodId.isAd() ? 0 : startPositionUs,
- /* contentPositionUs= */ startPositionUs);
+ newContentPositionUs = periodPosition.second;
+ newPeriodId = queue.resolveMediaPeriodIdForAds(periodPosition.first, newContentPositionUs);
+ } else if (oldContentPositionUs == C.TIME_UNSET && !timeline.isEmpty()) {
+ // Resolve unset start position to default position.
+ Pair defaultPosition =
+ getPeriodPosition(
+ timeline, timeline.getFirstWindowIndex(shuffleModeEnabled), C.TIME_UNSET);
+ newPeriodId = queue.resolveMediaPeriodIdForAds(defaultPosition.first, defaultPosition.second);
+ if (!newPeriodId.isAd()) {
+ // Keep unset start position if we need to play an ad first.
+ newContentPositionUs = defaultPosition.second;
}
- return;
- }
- MediaPeriodHolder periodHolder = queue.getFrontPeriod();
- long contentPositionUs = playbackInfo.contentPositionUs;
- Object playingPeriodUid =
- periodHolder == null ? playbackInfo.periodId.periodUid : periodHolder.uid;
- int periodIndex = timeline.getIndexOfPeriod(playingPeriodUid);
- if (periodIndex == C.INDEX_UNSET) {
- // We didn't find the current period in the new timeline. Attempt to resolve a subsequent
- // period whose window we can restart from.
- Object newPeriodUid = resolveSubsequentPeriod(playingPeriodUid, oldTimeline, timeline);
+ } else if (timeline.getIndexOfPeriod(newPeriodId.periodUid) == C.INDEX_UNSET) {
+ // The current period isn't in the new timeline. Attempt to resolve a subsequent period whose
+ // window we can restart from.
+ Object newPeriodUid = resolveSubsequentPeriod(newPeriodId.periodUid, oldTimeline, timeline);
if (newPeriodUid == null) {
// We failed to resolve a suitable restart position.
handleSourceInfoRefreshEndedPlayback();
return;
}
- // We resolved a subsequent period. Seek to the default position in the corresponding window.
+ // We resolved a subsequent period. Start at the default position in the corresponding window.
Pair defaultPosition =
getPeriodPosition(
timeline, timeline.getPeriodByUid(newPeriodUid, period).windowIndex, C.TIME_UNSET);
- newPeriodUid = defaultPosition.first;
- contentPositionUs = defaultPosition.second;
- MediaPeriodId periodId = queue.resolveMediaPeriodIdForAds(newPeriodUid, contentPositionUs);
+ newContentPositionUs = defaultPosition.second;
+ newPeriodId = queue.resolveMediaPeriodIdForAds(defaultPosition.first, newContentPositionUs);
+ } else if (newPeriodId.isAd()) {
+ // Recheck if the current ad still needs to be played.
+ newPeriodId = queue.resolveMediaPeriodIdForAds(newPeriodId.periodUid, newContentPositionUs);
+ }
+
+ if (playbackInfo.periodId.equals(newPeriodId) && oldContentPositionUs == newContentPositionUs) {
+ // We can keep the current playing period. Update the rest of the queued periods.
+ if (!queue.updateQueuedPeriods(rendererPositionUs, getMaxRendererReadPositionUs())) {
+ seekToCurrentPosition(/* sendDiscontinuity= */ false);
+ }
+ } else {
+ // Something changed. Seek to new start position.
+ MediaPeriodHolder periodHolder = queue.getFrontPeriod();
if (periodHolder != null) {
// Update the new playing media period info if it already exists.
- while (periodHolder.next != null) {
- periodHolder = periodHolder.next;
- if (periodHolder.info.id.equals(periodId)) {
+ while (periodHolder.getNext() != null) {
+ periodHolder = periodHolder.getNext();
+ if (periodHolder.info.id.equals(newPeriodId)) {
periodHolder.info = queue.getUpdatedMediaPeriodInfo(periodHolder.info);
}
}
}
// Actually do the seek.
- long seekPositionUs = seekToPeriodPosition(periodId, periodId.isAd() ? 0 : contentPositionUs);
- playbackInfo = playbackInfo.fromNewPosition(periodId, seekPositionUs, contentPositionUs);
- return;
+ long newPositionUs = newPeriodId.isAd() ? 0 : newContentPositionUs;
+ long seekedToPositionUs = seekToPeriodPosition(newPeriodId, newPositionUs);
+ playbackInfo =
+ playbackInfo.copyWithNewPosition(
+ newPeriodId, seekedToPositionUs, newContentPositionUs, getTotalBufferedDurationUs());
}
+ handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false);
+ }
- MediaPeriodId playingPeriodId = playbackInfo.periodId;
- if (playingPeriodId.isAd()) {
- MediaPeriodId periodId =
- queue.resolveMediaPeriodIdForAds(playingPeriodUid, contentPositionUs);
- if (!periodId.equals(playingPeriodId)) {
- // The previously playing ad should no longer be played, so skip it.
- long seekPositionUs =
- seekToPeriodPosition(periodId, periodId.isAd() ? 0 : contentPositionUs);
- playbackInfo = playbackInfo.fromNewPosition(periodId, seekPositionUs, contentPositionUs);
- return;
+ private long getMaxRendererReadPositionUs() {
+ MediaPeriodHolder readingHolder = queue.getReadingPeriod();
+ if (readingHolder == null) {
+ return 0;
+ }
+ long maxReadPositionUs = readingHolder.getRendererOffset();
+ for (int i = 0; i < renderers.length; i++) {
+ if (renderers[i].getState() == Renderer.STATE_DISABLED
+ || renderers[i].getStream() != readingHolder.sampleStreams[i]) {
+ // Ignore disabled renderers and renderers with sample streams from previous periods.
+ continue;
+ }
+ long readingPositionUs = renderers[i].getReadingPositionUs();
+ if (readingPositionUs == C.TIME_END_OF_SOURCE) {
+ return C.TIME_END_OF_SOURCE;
+ } else {
+ maxReadPositionUs = Math.max(readingPositionUs, maxReadPositionUs);
}
}
-
- if (!queue.updateQueuedPeriods(playingPeriodId, rendererPositionUs)) {
- seekToCurrentPosition(/* sendDiscontinuity= */ false);
- }
- updateLoadingMediaPeriodId();
+ return maxReadPositionUs;
}
private void handleSourceInfoRefreshEndedPlayback() {
setState(Player.STATE_ENDED);
// Reset, but retain the source so that it can still be used should a seek occur.
resetInternal(
- /* releaseMediaSource= */ false, /* resetPosition= */ true, /* resetState= */ false);
+ /* resetRenderers= */ false,
+ /* releaseMediaSource= */ false,
+ /* resetPosition= */ true,
+ /* resetState= */ false);
}
/**
@@ -1333,8 +1445,7 @@ import java.util.Collections;
seekPosition.windowPositionUs);
} catch (IndexOutOfBoundsException e) {
// The window index of the seek position was outside the bounds of the timeline.
- throw new IllegalSeekPositionException(timeline, seekPosition.windowIndex,
- seekPosition.windowPositionUs);
+ return null;
}
if (timeline == seekTimeline) {
// Our internal timeline is the seek timeline, so the mapped position is correct.
@@ -1378,94 +1489,89 @@ import java.util.Collections;
mediaSource.maybeThrowSourceInfoRefreshError();
return;
}
-
- // Update the loading period if required.
maybeUpdateLoadingPeriod();
+ maybeUpdateReadingPeriod();
+ maybeUpdatePlayingPeriod();
+ }
+
+ private void maybeUpdateLoadingPeriod() throws IOException {
+ queue.reevaluateBuffer(rendererPositionUs);
+ if (queue.shouldLoadNextMediaPeriod()) {
+ MediaPeriodInfo info = queue.getNextMediaPeriodInfo(rendererPositionUs, playbackInfo);
+ if (info == null) {
+ maybeThrowSourceInfoRefreshError();
+ } else {
+ MediaPeriod mediaPeriod =
+ queue.enqueueNextMediaPeriod(
+ rendererCapabilities,
+ trackSelector,
+ loadControl.getAllocator(),
+ mediaSource,
+ info,
+ emptyTrackSelectorResult);
+ mediaPeriod.prepare(this, info.startPositionUs);
+ setIsLoading(true);
+ handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false);
+ }
+ }
MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod();
if (loadingPeriodHolder == null || loadingPeriodHolder.isFullyBuffered()) {
setIsLoading(false);
} else if (!playbackInfo.isLoading) {
maybeContinueLoading();
}
+ }
- if (!queue.hasPlayingPeriod()) {
- // We're waiting for the first period to be prepared.
+ private void maybeUpdateReadingPeriod() throws ExoPlaybackException, IOException {
+ MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod();
+ if (readingPeriodHolder == null) {
return;
}
- // Advance the playing period if necessary.
- MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();
- MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod();
- boolean advancedPlayingPeriod = false;
- while (playWhenReady
- && playingPeriodHolder != readingPeriodHolder
- && rendererPositionUs >= playingPeriodHolder.next.getStartPositionRendererTime()) {
- // All enabled renderers' streams have been read to the end, and the playback position reached
- // the end of the playing period, so advance playback to the next period.
- if (advancedPlayingPeriod) {
- // If we advance more than one period at a time, notify listeners after each update.
- maybeNotifyPlaybackInfoChanged();
- }
- int discontinuityReason =
- playingPeriodHolder.info.isLastInTimelinePeriod
- ? Player.DISCONTINUITY_REASON_PERIOD_TRANSITION
- : Player.DISCONTINUITY_REASON_AD_INSERTION;
- MediaPeriodHolder oldPlayingPeriodHolder = playingPeriodHolder;
- playingPeriodHolder = queue.advancePlayingPeriod();
- updatePlayingPeriodRenderers(oldPlayingPeriodHolder);
- playbackInfo = playbackInfo.fromNewPosition(playingPeriodHolder.info.id,
- playingPeriodHolder.info.startPositionUs, playingPeriodHolder.info.contentPositionUs);
- playbackInfoUpdate.setPositionDiscontinuity(discontinuityReason);
- updatePlaybackPositions();
- advancedPlayingPeriod = true;
- }
-
- if (readingPeriodHolder.info.isFinal) {
- for (int i = 0; i < renderers.length; i++) {
- Renderer renderer = renderers[i];
- SampleStream sampleStream = readingPeriodHolder.sampleStreams[i];
- // Defer setting the stream as final until the renderer has actually consumed the whole
- // stream in case of playlist changes that cause the stream to be no longer final.
- if (sampleStream != null && renderer.getStream() == sampleStream
- && renderer.hasReadStreamToEnd()) {
- renderer.setCurrentStreamFinal();
+ if (readingPeriodHolder.getNext() == null) {
+ // We don't have a successor to advance the reading period to.
+ if (readingPeriodHolder.info.isFinal) {
+ for (int i = 0; i < renderers.length; i++) {
+ Renderer renderer = renderers[i];
+ SampleStream sampleStream = readingPeriodHolder.sampleStreams[i];
+ // Defer setting the stream as final until the renderer has actually consumed the whole
+ // stream in case of playlist changes that cause the stream to be no longer final.
+ if (sampleStream != null
+ && renderer.getStream() == sampleStream
+ && renderer.hasReadStreamToEnd()) {
+ renderer.setCurrentStreamFinal();
+ }
}
}
return;
}
- // Advance the reading period if necessary.
- if (readingPeriodHolder.next == null || !readingPeriodHolder.next.prepared) {
- // We don't have a successor to advance the reading period to.
+ if (!hasReadingPeriodFinishedReading()) {
return;
}
- for (int i = 0; i < renderers.length; i++) {
- Renderer renderer = renderers[i];
- SampleStream sampleStream = readingPeriodHolder.sampleStreams[i];
- if (renderer.getStream() != sampleStream
- || (sampleStream != null && !renderer.hasReadStreamToEnd())) {
- // The current reading period is still being read by at least one renderer.
- return;
- }
+ if (!readingPeriodHolder.getNext().prepared) {
+ // The successor is not prepared yet.
+ maybeThrowPeriodPrepareError();
+ return;
}
- TrackSelectorResult oldTrackSelectorResult = readingPeriodHolder.trackSelectorResult;
+ TrackSelectorResult oldTrackSelectorResult = readingPeriodHolder.getTrackSelectorResult();
readingPeriodHolder = queue.advanceReadingPeriod();
- TrackSelectorResult newTrackSelectorResult = readingPeriodHolder.trackSelectorResult;
+ TrackSelectorResult newTrackSelectorResult = readingPeriodHolder.getTrackSelectorResult();
- boolean initialDiscontinuity =
- readingPeriodHolder.mediaPeriod.readDiscontinuity() != C.TIME_UNSET;
+ if (readingPeriodHolder.mediaPeriod.readDiscontinuity() != C.TIME_UNSET) {
+ // The new period starts with a discontinuity, so the renderers will play out all data, then
+ // be disabled and re-enabled when they start playing the next period.
+ setAllRendererStreamsFinal();
+ return;
+ }
for (int i = 0; i < renderers.length; i++) {
Renderer renderer = renderers[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) {
- // The new period starts with a discontinuity, so the renderer will play out all data then
- // be disabled and re-enabled when it starts playing the next period.
- renderer.setCurrentStreamFinal();
- } else if (!renderer.isCurrentStreamFinal()) {
+ if (rendererWasEnabled && !renderer.isCurrentStreamFinal()) {
+ // The renderer is enabled and its stream is not final, so we still have a chance to replace
+ // the sample streams.
TrackSelection newSelection = newTrackSelectorResult.selections.get(i);
boolean newRendererEnabled = newTrackSelectorResult.isRendererEnabled(i);
boolean isNoSampleRenderer = rendererCapabilities[i].getTrackType() == C.TRACK_TYPE_NONE;
@@ -1493,23 +1599,67 @@ import java.util.Collections;
}
}
- private void maybeUpdateLoadingPeriod() throws IOException {
- queue.reevaluateBuffer(rendererPositionUs);
- if (queue.shouldLoadNextMediaPeriod()) {
- MediaPeriodInfo info = queue.getNextMediaPeriodInfo(rendererPositionUs, playbackInfo);
- if (info == null) {
- mediaSource.maybeThrowSourceInfoRefreshError();
- } else {
- MediaPeriod mediaPeriod =
- queue.enqueueNextMediaPeriod(
- rendererCapabilities,
- trackSelector,
- loadControl.getAllocator(),
- mediaSource,
- info);
- mediaPeriod.prepare(this, info.startPositionUs);
- setIsLoading(true);
- updateLoadingMediaPeriodId();
+ private void maybeUpdatePlayingPeriod() throws ExoPlaybackException {
+ boolean advancedPlayingPeriod = false;
+ while (shouldAdvancePlayingPeriod()) {
+ if (advancedPlayingPeriod) {
+ // If we advance more than one period at a time, notify listeners after each update.
+ maybeNotifyPlaybackInfoChanged();
+ }
+ MediaPeriodHolder oldPlayingPeriodHolder = queue.getPlayingPeriod();
+ MediaPeriodHolder newPlayingPeriodHolder = queue.advancePlayingPeriod();
+ updatePlayingPeriodRenderers(oldPlayingPeriodHolder);
+ playbackInfo =
+ playbackInfo.copyWithNewPosition(
+ newPlayingPeriodHolder.info.id,
+ newPlayingPeriodHolder.info.startPositionUs,
+ newPlayingPeriodHolder.info.contentPositionUs,
+ getTotalBufferedDurationUs());
+ int discontinuityReason =
+ oldPlayingPeriodHolder.info.isLastInTimelinePeriod
+ ? Player.DISCONTINUITY_REASON_PERIOD_TRANSITION
+ : Player.DISCONTINUITY_REASON_AD_INSERTION;
+ playbackInfoUpdate.setPositionDiscontinuity(discontinuityReason);
+ updatePlaybackPositions();
+ advancedPlayingPeriod = true;
+ }
+ }
+
+ private boolean shouldAdvancePlayingPeriod() {
+ if (!playWhenReady) {
+ return false;
+ }
+ MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();
+ if (playingPeriodHolder == null) {
+ return false;
+ }
+ MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod();
+ if (playingPeriodHolder == readingPeriodHolder) {
+ return false;
+ }
+ MediaPeriodHolder nextPlayingPeriodHolder =
+ Assertions.checkNotNull(playingPeriodHolder.getNext());
+ return rendererPositionUs >= nextPlayingPeriodHolder.getStartPositionRendererTime();
+ }
+
+ private boolean hasReadingPeriodFinishedReading() {
+ MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod();
+ for (int i = 0; i < renderers.length; i++) {
+ Renderer renderer = renderers[i];
+ SampleStream sampleStream = readingPeriodHolder.sampleStreams[i];
+ if (renderer.getStream() != sampleStream
+ || (sampleStream != null && !renderer.hasReadStreamToEnd())) {
+ // The current reading period is still being read by at least one renderer.
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private void setAllRendererStreamsFinal() {
+ for (Renderer renderer : renderers) {
+ if (renderer.getStream() != null) {
+ renderer.setCurrentStreamFinal();
}
}
}
@@ -1520,9 +1670,10 @@ import java.util.Collections;
return;
}
MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod();
- loadingPeriodHolder.handlePrepared(mediaClock.getPlaybackParameters().speed);
+ loadingPeriodHolder.handlePrepared(
+ mediaClock.getPlaybackParameters().speed, playbackInfo.timeline);
updateLoadControlTrackSelection(
- loadingPeriodHolder.trackGroups, loadingPeriodHolder.trackSelectorResult);
+ loadingPeriodHolder.getTrackGroups(), loadingPeriodHolder.getTrackSelectorResult());
if (!queue.hasPlayingPeriod()) {
// This is the first prepared period, so start playing it.
MediaPeriodHolder playingPeriodHolder = queue.advancePlayingPeriod();
@@ -1541,9 +1692,13 @@ import java.util.Collections;
maybeContinueLoading();
}
- private void handlePlaybackParameters(PlaybackParameters playbackParameters)
+ private void handlePlaybackParameters(
+ PlaybackParameters playbackParameters, boolean acknowledgeCommand)
throws ExoPlaybackException {
- eventHandler.obtainMessage(MSG_PLAYBACK_PARAMETERS_CHANGED, playbackParameters).sendToTarget();
+ eventHandler
+ .obtainMessage(
+ MSG_PLAYBACK_PARAMETERS_CHANGED, acknowledgeCommand ? 1 : 0, 0, playbackParameters)
+ .sendToTarget();
updateTrackSelectionPlaybackSpeed(playbackParameters.speed);
for (Renderer renderer : renderers) {
if (renderer != null) {
@@ -1560,7 +1715,7 @@ import java.util.Collections;
return;
}
long bufferedDurationUs =
- nextLoadPositionUs - loadingPeriodHolder.toPeriodTime(rendererPositionUs);
+ getTotalBufferedDurationUs(/* bufferedPositionInLoadingPeriodUs= */ nextLoadPositionUs);
boolean continueLoading =
loadControl.shouldContinueLoading(
bufferedDurationUs, mediaClock.getPlaybackParameters().speed);
@@ -1582,11 +1737,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.isRendererEnabled(i)) {
+ if (newPlayingPeriodHolder.getTrackSelectorResult().isRendererEnabled(i)) {
enabledRendererCount++;
}
if (rendererWasEnabledFlags[i]
- && (!newPlayingPeriodHolder.trackSelectorResult.isRendererEnabled(i)
+ && (!newPlayingPeriodHolder.getTrackSelectorResult().isRendererEnabled(i)
|| (renderer.isCurrentStreamFinal()
&& renderer.getStream() == oldPlayingPeriodHolder.sampleStreams[i]))) {
// The renderer should be disabled before playing the next period, either because it's not
@@ -1597,7 +1752,8 @@ import java.util.Collections;
}
playbackInfo =
playbackInfo.copyWithTrackInfo(
- newPlayingPeriodHolder.trackGroups, newPlayingPeriodHolder.trackSelectorResult);
+ newPlayingPeriodHolder.getTrackGroups(),
+ newPlayingPeriodHolder.getTrackSelectorResult());
enableRenderers(rendererWasEnabledFlags, enabledRendererCount);
}
@@ -1605,9 +1761,17 @@ import java.util.Collections;
throws ExoPlaybackException {
enabledRenderers = new Renderer[totalEnabledRendererCount];
int enabledRendererCount = 0;
- MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();
+ TrackSelectorResult trackSelectorResult = queue.getPlayingPeriod().getTrackSelectorResult();
+ // Reset all disabled renderers before enabling any new ones. This makes sure resources released
+ // by the disabled renderers will be available to renderers that are being enabled.
for (int i = 0; i < renderers.length; i++) {
- if (playingPeriodHolder.trackSelectorResult.isRendererEnabled(i)) {
+ if (!trackSelectorResult.isRendererEnabled(i)) {
+ renderers[i].reset();
+ }
+ }
+ // Enable the renderers.
+ for (int i = 0; i < renderers.length; i++) {
+ if (trackSelectorResult.isRendererEnabled(i)) {
enableRenderer(i, rendererWasEnabledFlags[i], enabledRendererCount++);
}
}
@@ -1620,10 +1784,10 @@ import java.util.Collections;
Renderer renderer = renderers[rendererIndex];
enabledRenderers[enabledRendererIndex] = renderer;
if (renderer.getState() == Renderer.STATE_DISABLED) {
+ TrackSelectorResult trackSelectorResult = playingPeriodHolder.getTrackSelectorResult();
RendererConfiguration rendererConfiguration =
- playingPeriodHolder.trackSelectorResult.rendererConfigurations[rendererIndex];
- TrackSelection newSelection = playingPeriodHolder.trackSelectorResult.selections.get(
- rendererIndex);
+ trackSelectorResult.rendererConfigurations[rendererIndex];
+ TrackSelection newSelection = trackSelectorResult.selections.get(rendererIndex);
Format[] formats = getFormats(newSelection);
// The renderer needs enabling with its new track selection.
boolean playing = playWhenReady && playbackInfo.playbackState == Player.STATE_READY;
@@ -1643,18 +1807,63 @@ import java.util.Collections;
private boolean rendererWaitingForNextStream(Renderer renderer) {
MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod();
- return readingPeriodHolder.next != null && readingPeriodHolder.next.prepared
- && renderer.hasReadStreamToEnd();
+ MediaPeriodHolder nextPeriodHolder = readingPeriodHolder.getNext();
+ return nextPeriodHolder != null && nextPeriodHolder.prepared && renderer.hasReadStreamToEnd();
}
- private void updateLoadingMediaPeriodId() {
+ private void handleLoadingMediaPeriodChanged(boolean loadingTrackSelectionChanged) {
MediaPeriodHolder loadingMediaPeriodHolder = queue.getLoadingPeriod();
MediaPeriodId loadingMediaPeriodId =
loadingMediaPeriodHolder == null ? playbackInfo.periodId : loadingMediaPeriodHolder.info.id;
- playbackInfo = playbackInfo.copyWithLoadingMediaPeriodId(loadingMediaPeriodId);
+ boolean loadingMediaPeriodChanged =
+ !playbackInfo.loadingMediaPeriodId.equals(loadingMediaPeriodId);
+ if (loadingMediaPeriodChanged) {
+ playbackInfo = playbackInfo.copyWithLoadingMediaPeriodId(loadingMediaPeriodId);
+ }
+ playbackInfo.bufferedPositionUs =
+ loadingMediaPeriodHolder == null
+ ? playbackInfo.positionUs
+ : loadingMediaPeriodHolder.getBufferedPositionUs();
+ playbackInfo.totalBufferedDurationUs = getTotalBufferedDurationUs();
+ if ((loadingMediaPeriodChanged || loadingTrackSelectionChanged)
+ && loadingMediaPeriodHolder != null
+ && loadingMediaPeriodHolder.prepared) {
+ updateLoadControlTrackSelection(
+ loadingMediaPeriodHolder.getTrackGroups(),
+ loadingMediaPeriodHolder.getTrackSelectorResult());
+ }
+ }
+
+ private long getTotalBufferedDurationUs() {
+ return getTotalBufferedDurationUs(playbackInfo.bufferedPositionUs);
+ }
+
+ private long getTotalBufferedDurationUs(long bufferedPositionInLoadingPeriodUs) {
+ MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod();
+ if (loadingPeriodHolder == null) {
+ return 0;
+ }
+ long totalBufferedDurationUs =
+ bufferedPositionInLoadingPeriodUs - loadingPeriodHolder.toPeriodTime(rendererPositionUs);
+ return Math.max(0, totalBufferedDurationUs);
+ }
+
+ private void updateLoadControlTrackSelection(
+ TrackGroupArray trackGroups, TrackSelectorResult trackSelectorResult) {
+ loadControl.onTracksSelected(renderers, trackGroups, trackSelectorResult.selections);
+ }
+
+ private void sendPlaybackParametersChangedInternal(
+ PlaybackParameters playbackParameters, boolean acknowledgeCommand) {
+ handler
+ .obtainMessage(
+ MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL,
+ acknowledgeCommand ? 1 : 0,
+ 0,
+ playbackParameters)
+ .sendToTarget();
}
- @NonNull
private static Format[] getFormats(TrackSelection newSelection) {
// Build an array of formats contained by the selection.
int length = newSelection != null ? newSelection.length() : 0;
@@ -1684,7 +1893,7 @@ import java.util.Collections;
public int resolvedPeriodIndex;
public long resolvedPeriodTimeUs;
- public @Nullable Object resolvedPeriodUid;
+ @Nullable public Object resolvedPeriodUid;
public PendingMessageInfo(PlayerMessage message) {
this.message = message;
@@ -1719,12 +1928,10 @@ import java.util.Collections;
public final MediaSource source;
public final Timeline timeline;
- public final Object manifest;
- public MediaSourceRefreshInfo(MediaSource source, Timeline timeline, Object manifest) {
+ public MediaSourceRefreshInfo(MediaSource source, Timeline timeline) {
this.source = source;
this.timeline = timeline;
- this.manifest = manifest;
}
}
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 b8bf0e8813..f420f20767 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.8.4";
+ public static final String VERSION = "2.10.4";
/** 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.8.4";
+ public static final String VERSION_SLASHY = "ExoPlayerLib/2.10.4";
/**
* 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 = 2008004;
+ public static final int VERSION_INT = 2010004;
/**
* 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 4c6e1838a8..b2bd20f0fe 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/Format.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/Format.java
@@ -17,8 +17,10 @@ package com.google.android.exoplayer2;
import android.os.Parcel;
import android.os.Parcelable;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.drm.DrmInitData;
+import com.google.android.exoplayer2.drm.DrmSession;
+import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
@@ -45,23 +47,26 @@ 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 @Nullable String id;
+ @Nullable public final String id;
/** The human readable label, or null if unknown or not applicable. */
- public final @Nullable String label;
-
+ @Nullable public final String label;
+ /** Track selection flags. */
+ @C.SelectionFlags public final int selectionFlags;
+ /** Track role flags. */
+ @C.RoleFlags public final int roleFlags;
/**
* 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 @Nullable String codecs;
+ @Nullable public final String codecs;
/** Metadata, or null if unknown or not applicable. */
- public final @Nullable Metadata metadata;
+ @Nullable public final Metadata metadata;
// Container specific.
/** The mime type of the container, or null if unknown or not applicable. */
- public final @Nullable String containerMimeType;
+ @Nullable public final String containerMimeType;
// Elementary stream specific.
@@ -69,7 +74,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 @Nullable String sampleMimeType;
+ @Nullable public final String sampleMimeType;
/**
* The maximum size of a buffer of data (typically one sample), or {@link #NO_VALUE} if unknown or
* not applicable.
@@ -81,7 +86,7 @@ public final class Format implements Parcelable {
*/
public final List initializationData;
/** DRM initialization data if the stream is protected, or null otherwise. */
- public final @Nullable DrmInitData drmInitData;
+ @Nullable public final DrmInitData drmInitData;
/**
* For samples that contain subsamples, this is an offset that should be added to subsample
@@ -119,9 +124,9 @@ 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 @Nullable byte[] projectionData;
+ @Nullable public final byte[] projectionData;
/** The color metadata associated with the video, helps with accurate color reproduction. */
- public final @Nullable ColorInfo colorInfo;
+ @Nullable public final ColorInfo colorInfo;
// Audio specific.
@@ -153,25 +158,31 @@ public final class Format implements Parcelable {
// Audio and text specific.
- /**
- * Track selection flags.
- */
- @C.SelectionFlags
- public final int selectionFlags;
-
- /** The language, or null if unknown or not applicable. */
- public final @Nullable String language;
-
+ /** The language as an IETF BCP 47 conformant tag, or null if unknown or not applicable. */
+ @Nullable public final String language;
/**
* The Accessibility channel, or {@link #NO_VALUE} if not known or applicable.
*/
public final int accessibilityChannel;
+ // Provided by source.
+
+ /**
+ * The type of the {@link ExoMediaCrypto} provided by the media source, if the media source can
+ * acquire a {@link DrmSession} for {@link #drmInitData}. Null if the media source cannot acquire
+ * a session for {@link #drmInitData}, or if not applicable.
+ */
+ @Nullable public final Class extends ExoMediaCrypto> exoMediaCryptoType;
+
// Lazily initialized hashcode.
private int hashCode;
// Video.
+ /**
+ * @deprecated Use {@link #createVideoContainerFormat(String, String, String, String, String, int,
+ * int, int, float, List, int, int)} instead.
+ */
@Deprecated
public static Format createVideoContainerFormat(
@Nullable String id,
@@ -195,7 +206,8 @@ public final class Format implements Parcelable {
height,
frameRate,
initializationData,
- selectionFlags);
+ selectionFlags,
+ /* roleFlags= */ 0);
}
public static Format createVideoContainerFormat(
@@ -209,15 +221,22 @@ public final class Format implements Parcelable {
int height,
float frameRate,
@Nullable List initializationData,
- @C.SelectionFlags int selectionFlags) {
+ @C.SelectionFlags int selectionFlags,
+ @C.RoleFlags int roleFlags) {
return new Format(
id,
label,
+ selectionFlags,
+ roleFlags,
+ bitrate,
+ codecs,
+ /* metadata= */ null,
containerMimeType,
sampleMimeType,
- codecs,
- bitrate,
/* maxInputSize= */ NO_VALUE,
+ initializationData,
+ /* drmInitData= */ null,
+ OFFSET_SAMPLE_RELATIVE,
width,
height,
frameRate,
@@ -231,13 +250,9 @@ public final class Format implements Parcelable {
/* pcmEncoding= */ NO_VALUE,
/* encoderDelay= */ NO_VALUE,
/* encoderPadding= */ NO_VALUE,
- selectionFlags,
/* language= */ null,
/* accessibilityChannel= */ NO_VALUE,
- OFFSET_SAMPLE_RELATIVE,
- initializationData,
- /* drmInitData= */ null,
- /* metadata= */ null);
+ /* exoMediaCryptoType= */ null);
}
public static Format createVideoSampleFormat(
@@ -316,11 +331,17 @@ public final class Format implements Parcelable {
return new Format(
id,
/* label= */ null,
+ /* selectionFlags= */ 0,
+ /* roleFlags= */ 0,
+ bitrate,
+ codecs,
+ /* metadata= */ null,
/* containerMimeType= */ null,
sampleMimeType,
- codecs,
- bitrate,
maxInputSize,
+ initializationData,
+ drmInitData,
+ OFFSET_SAMPLE_RELATIVE,
width,
height,
frameRate,
@@ -334,17 +355,17 @@ public final class Format implements Parcelable {
/* pcmEncoding= */ NO_VALUE,
/* encoderDelay= */ NO_VALUE,
/* encoderPadding= */ NO_VALUE,
- /* selectionFlags= */ 0,
/* language= */ null,
/* accessibilityChannel= */ NO_VALUE,
- OFFSET_SAMPLE_RELATIVE,
- initializationData,
- drmInitData,
- /* metadata= */ null);
+ /* exoMediaCryptoType= */ null);
}
// Audio.
+ /**
+ * @deprecated Use {@link #createAudioContainerFormat(String, String, String, String, String, int,
+ * int, int, List, int, int, String)} instead.
+ */
@Deprecated
public static Format createAudioContainerFormat(
@Nullable String id,
@@ -368,6 +389,7 @@ public final class Format implements Parcelable {
sampleRate,
initializationData,
selectionFlags,
+ /* roleFlags= */ 0,
language);
}
@@ -382,15 +404,22 @@ public final class Format implements Parcelable {
int sampleRate,
@Nullable List initializationData,
@C.SelectionFlags int selectionFlags,
+ @C.RoleFlags int roleFlags,
@Nullable String language) {
return new Format(
id,
label,
+ selectionFlags,
+ roleFlags,
+ bitrate,
+ codecs,
+ /* metadata= */ null,
containerMimeType,
sampleMimeType,
- codecs,
- bitrate,
/* maxInputSize= */ NO_VALUE,
+ initializationData,
+ /* drmInitData= */ null,
+ OFFSET_SAMPLE_RELATIVE,
/* width= */ NO_VALUE,
/* height= */ NO_VALUE,
/* frameRate= */ NO_VALUE,
@@ -404,13 +433,9 @@ public final class Format implements Parcelable {
/* pcmEncoding= */ NO_VALUE,
/* encoderDelay= */ NO_VALUE,
/* encoderPadding= */ NO_VALUE,
- selectionFlags,
language,
/* accessibilityChannel= */ NO_VALUE,
- OFFSET_SAMPLE_RELATIVE,
- initializationData,
- /* drmInitData= */ null,
- /* metadata= */ null);
+ /* exoMediaCryptoType= */ null);
}
public static Format createAudioSampleFormat(
@@ -490,11 +515,17 @@ public final class Format implements Parcelable {
return new Format(
id,
/* label= */ null,
+ selectionFlags,
+ /* roleFlags= */ 0,
+ bitrate,
+ codecs,
+ metadata,
/* containerMimeType= */ null,
sampleMimeType,
- codecs,
- bitrate,
maxInputSize,
+ initializationData,
+ drmInitData,
+ OFFSET_SAMPLE_RELATIVE,
/* width= */ NO_VALUE,
/* height= */ NO_VALUE,
/* frameRate= */ NO_VALUE,
@@ -508,37 +539,13 @@ public final class Format implements Parcelable {
pcmEncoding,
encoderDelay,
encoderPadding,
- selectionFlags,
language,
/* accessibilityChannel= */ NO_VALUE,
- OFFSET_SAMPLE_RELATIVE,
- initializationData,
- drmInitData,
- metadata);
+ /* exoMediaCryptoType= */ null);
}
// Text.
- @Deprecated
- 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,
- /* label= */ null,
- containerMimeType,
- sampleMimeType,
- codecs,
- bitrate,
- selectionFlags,
- language);
- }
-
public static Format createTextContainerFormat(
@Nullable String id,
@Nullable String label,
@@ -547,6 +554,7 @@ public final class Format implements Parcelable {
@Nullable String codecs,
int bitrate,
@C.SelectionFlags int selectionFlags,
+ @C.RoleFlags int roleFlags,
@Nullable String language) {
return createTextContainerFormat(
id,
@@ -556,6 +564,7 @@ public final class Format implements Parcelable {
codecs,
bitrate,
selectionFlags,
+ roleFlags,
language,
/* accessibilityChannel= */ NO_VALUE);
}
@@ -568,16 +577,23 @@ public final class Format implements Parcelable {
@Nullable String codecs,
int bitrate,
@C.SelectionFlags int selectionFlags,
+ @C.RoleFlags int roleFlags,
@Nullable String language,
int accessibilityChannel) {
return new Format(
id,
label,
+ selectionFlags,
+ roleFlags,
+ bitrate,
+ codecs,
+ /* metadata= */ null,
containerMimeType,
sampleMimeType,
- codecs,
- bitrate,
/* maxInputSize= */ NO_VALUE,
+ /* initializationData= */ null,
+ /* drmInitData= */ null,
+ OFFSET_SAMPLE_RELATIVE,
/* width= */ NO_VALUE,
/* height= */ NO_VALUE,
/* frameRate= */ NO_VALUE,
@@ -591,13 +607,9 @@ public final class Format implements Parcelable {
/* pcmEncoding= */ NO_VALUE,
/* encoderDelay= */ NO_VALUE,
/* encoderPadding= */ NO_VALUE,
- selectionFlags,
language,
accessibilityChannel,
- OFFSET_SAMPLE_RELATIVE,
- /* initializationData= */ null,
- /* drmInitData= */ null,
- /* metadata= */ null);
+ /* exoMediaCryptoType= */ null);
}
public static Format createTextSampleFormat(
@@ -685,11 +697,17 @@ public final class Format implements Parcelable {
return new Format(
id,
/* label= */ null,
+ selectionFlags,
+ /* roleFlags= */ 0,
+ bitrate,
+ codecs,
+ /* metadata= */ null,
/* containerMimeType= */ null,
sampleMimeType,
- codecs,
- bitrate,
/* maxInputSize= */ NO_VALUE,
+ initializationData,
+ drmInitData,
+ subsampleOffsetUs,
/* width= */ NO_VALUE,
/* height= */ NO_VALUE,
/* frameRate= */ NO_VALUE,
@@ -703,13 +721,9 @@ public final class Format implements Parcelable {
/* pcmEncoding= */ NO_VALUE,
/* encoderDelay= */ NO_VALUE,
/* encoderPadding= */ NO_VALUE,
- selectionFlags,
language,
accessibilityChannel,
- subsampleOffsetUs,
- initializationData,
- drmInitData,
- /* metadata= */ null);
+ /* exoMediaCryptoType= */ null);
}
// Image.
@@ -726,11 +740,17 @@ public final class Format implements Parcelable {
return new Format(
id,
/* label= */ null,
+ selectionFlags,
+ /* roleFlags= */ 0,
+ bitrate,
+ codecs,
+ /* metadata=*/ null,
/* containerMimeType= */ null,
sampleMimeType,
- codecs,
- bitrate,
/* maxInputSize= */ NO_VALUE,
+ initializationData,
+ drmInitData,
+ OFFSET_SAMPLE_RELATIVE,
/* width= */ NO_VALUE,
/* height= */ NO_VALUE,
/* frameRate= */ NO_VALUE,
@@ -744,17 +764,17 @@ public final class Format implements Parcelable {
/* pcmEncoding= */ NO_VALUE,
/* encoderDelay= */ NO_VALUE,
/* encoderPadding= */ NO_VALUE,
- selectionFlags,
language,
/* accessibilityChannel= */ NO_VALUE,
- OFFSET_SAMPLE_RELATIVE,
- initializationData,
- drmInitData,
- /* metadata=*/ null);
+ /* exoMediaCryptoType= */ null);
}
// Generic.
+ /**
+ * @deprecated Use {@link #createContainerFormat(String, String, String, String, String, int, int,
+ * int, String)} instead.
+ */
@Deprecated
public static Format createContainerFormat(
@Nullable String id,
@@ -772,6 +792,7 @@ public final class Format implements Parcelable {
codecs,
bitrate,
selectionFlags,
+ /* roleFlags= */ 0,
language);
}
@@ -783,15 +804,22 @@ public final class Format implements Parcelable {
@Nullable String codecs,
int bitrate,
@C.SelectionFlags int selectionFlags,
+ @C.RoleFlags int roleFlags,
@Nullable String language) {
return new Format(
id,
label,
+ selectionFlags,
+ roleFlags,
+ bitrate,
+ codecs,
+ /* metadata= */ null,
containerMimeType,
sampleMimeType,
- codecs,
- bitrate,
/* maxInputSize= */ NO_VALUE,
+ /* initializationData= */ null,
+ /* drmInitData= */ null,
+ OFFSET_SAMPLE_RELATIVE,
/* width= */ NO_VALUE,
/* height= */ NO_VALUE,
/* frameRate= */ NO_VALUE,
@@ -805,13 +833,9 @@ public final class Format implements Parcelable {
/* pcmEncoding= */ NO_VALUE,
/* encoderDelay= */ NO_VALUE,
/* encoderPadding= */ NO_VALUE,
- selectionFlags,
language,
/* accessibilityChannel= */ NO_VALUE,
- OFFSET_SAMPLE_RELATIVE,
- /* initializationData= */ null,
- /* drmInitData= */ null,
- /* metadata= */ null);
+ /* exoMediaCryptoType= */ null);
}
public static Format createSampleFormat(
@@ -819,11 +843,17 @@ public final class Format implements Parcelable {
return new Format(
id,
/* label= */ null,
+ /* selectionFlags= */ 0,
+ /* roleFlags= */ 0,
+ /* bitrate= */ NO_VALUE,
+ /* codecs= */ null,
+ /* metadata= */ null,
/* containerMimeType= */ null,
sampleMimeType,
- /* codecs= */ null,
- /* bitrate= */ NO_VALUE,
/* maxInputSize= */ NO_VALUE,
+ /* initializationData= */ null,
+ /* drmInitData= */ null,
+ subsampleOffsetUs,
/* width= */ NO_VALUE,
/* height= */ NO_VALUE,
/* frameRate= */ NO_VALUE,
@@ -837,13 +867,9 @@ public final class Format implements Parcelable {
/* pcmEncoding= */ NO_VALUE,
/* encoderDelay= */ NO_VALUE,
/* encoderPadding= */ NO_VALUE,
- /* selectionFlags= */ 0,
/* language= */ null,
/* accessibilityChannel= */ NO_VALUE,
- subsampleOffsetUs,
- /* initializationData= */ null,
- /* drmInitData= */ null,
- /* metadata= */ null);
+ /* exoMediaCryptoType= */ null);
}
public static Format createSampleFormat(
@@ -855,11 +881,17 @@ public final class Format implements Parcelable {
return new Format(
id,
/* label= */ null,
+ /* selectionFlags= */ 0,
+ /* roleFlags= */ 0,
+ bitrate,
+ codecs,
+ /* metadata= */ null,
/* containerMimeType= */ null,
sampleMimeType,
- codecs,
- bitrate,
/* maxInputSize= */ NO_VALUE,
+ /* initializationData= */ null,
+ drmInitData,
+ OFFSET_SAMPLE_RELATIVE,
/* width= */ NO_VALUE,
/* height= */ NO_VALUE,
/* frameRate= */ NO_VALUE,
@@ -873,23 +905,28 @@ public final class Format implements Parcelable {
/* pcmEncoding= */ NO_VALUE,
/* encoderDelay= */ NO_VALUE,
/* encoderPadding= */ NO_VALUE,
- /* selectionFlags= */ 0,
/* language= */ null,
/* accessibilityChannel= */ NO_VALUE,
- OFFSET_SAMPLE_RELATIVE,
- /* initializationData= */ null,
- drmInitData,
- /* metadata= */ null);
+ /* exoMediaCryptoType= */ null);
}
/* package */ Format(
@Nullable String id,
@Nullable String label,
- @Nullable String containerMimeType,
- @Nullable String sampleMimeType,
- @Nullable String codecs,
+ @C.SelectionFlags int selectionFlags,
+ @C.RoleFlags int roleFlags,
int bitrate,
+ @Nullable String codecs,
+ @Nullable Metadata metadata,
+ // Container specific.
+ @Nullable String containerMimeType,
+ // Elementary stream specific.
+ @Nullable String sampleMimeType,
int maxInputSize,
+ @Nullable List initializationData,
+ @Nullable DrmInitData drmInitData,
+ long subsampleOffsetUs,
+ // Video specific.
int width,
int height,
float frameRate,
@@ -898,25 +935,34 @@ public final class Format implements Parcelable {
@Nullable byte[] projectionData,
@C.StereoMode int stereoMode,
@Nullable ColorInfo colorInfo,
+ // Audio specific.
int channelCount,
int sampleRate,
@C.PcmEncoding int pcmEncoding,
int encoderDelay,
int encoderPadding,
- @C.SelectionFlags int selectionFlags,
+ // Audio and text specific.
@Nullable String language,
int accessibilityChannel,
- long subsampleOffsetUs,
- @Nullable List initializationData,
- @Nullable DrmInitData drmInitData,
- @Nullable Metadata metadata) {
+ // Provided by source.
+ @Nullable Class extends ExoMediaCrypto> exoMediaCryptoType) {
this.id = id;
this.label = label;
- this.containerMimeType = containerMimeType;
- this.sampleMimeType = sampleMimeType;
- this.codecs = codecs;
+ this.selectionFlags = selectionFlags;
+ this.roleFlags = roleFlags;
this.bitrate = bitrate;
+ this.codecs = codecs;
+ this.metadata = metadata;
+ // Container specific.
+ this.containerMimeType = containerMimeType;
+ // Elementary stream specific.
+ this.sampleMimeType = sampleMimeType;
this.maxInputSize = maxInputSize;
+ this.initializationData =
+ initializationData == null ? Collections.emptyList() : initializationData;
+ this.drmInitData = drmInitData;
+ this.subsampleOffsetUs = subsampleOffsetUs;
+ // Video specific.
this.width = width;
this.height = height;
this.frameRate = frameRate;
@@ -926,30 +972,41 @@ public final class Format implements Parcelable {
this.projectionData = projectionData;
this.stereoMode = stereoMode;
this.colorInfo = colorInfo;
+ // Audio specific.
this.channelCount = channelCount;
this.sampleRate = sampleRate;
this.pcmEncoding = pcmEncoding;
this.encoderDelay = encoderDelay == Format.NO_VALUE ? 0 : encoderDelay;
this.encoderPadding = encoderPadding == Format.NO_VALUE ? 0 : encoderPadding;
- this.selectionFlags = selectionFlags;
- this.language = language;
+ // Audio and text specific.
+ this.language = Util.normalizeLanguageCode(language);
this.accessibilityChannel = accessibilityChannel;
- this.subsampleOffsetUs = subsampleOffsetUs;
- this.initializationData =
- initializationData == null ? Collections.emptyList() : initializationData;
- this.drmInitData = drmInitData;
- this.metadata = metadata;
+ // Provided by source.
+ this.exoMediaCryptoType = exoMediaCryptoType;
}
@SuppressWarnings("ResourceType")
/* package */ Format(Parcel in) {
id = in.readString();
label = in.readString();
- containerMimeType = in.readString();
- sampleMimeType = in.readString();
- codecs = in.readString();
+ selectionFlags = in.readInt();
+ roleFlags = in.readInt();
bitrate = in.readInt();
+ codecs = in.readString();
+ metadata = in.readParcelable(Metadata.class.getClassLoader());
+ // Container specific.
+ containerMimeType = in.readString();
+ // Elementary stream specific.
+ sampleMimeType = in.readString();
maxInputSize = in.readInt();
+ int initializationDataSize = in.readInt();
+ initializationData = new ArrayList<>(initializationDataSize);
+ for (int i = 0; i < initializationDataSize; i++) {
+ initializationData.add(in.createByteArray());
+ }
+ drmInitData = in.readParcelable(DrmInitData.class.getClassLoader());
+ subsampleOffsetUs = in.readLong();
+ // Video specific.
width = in.readInt();
height = in.readInt();
frameRate = in.readFloat();
@@ -959,33 +1016,34 @@ public final class Format implements Parcelable {
projectionData = hasProjectionData ? in.createByteArray() : null;
stereoMode = in.readInt();
colorInfo = in.readParcelable(ColorInfo.class.getClassLoader());
+ // Audio specific.
channelCount = in.readInt();
sampleRate = in.readInt();
pcmEncoding = in.readInt();
encoderDelay = in.readInt();
encoderPadding = in.readInt();
- selectionFlags = in.readInt();
+ // Audio and text specific.
language = in.readString();
accessibilityChannel = in.readInt();
- subsampleOffsetUs = in.readLong();
- int initializationDataSize = in.readInt();
- initializationData = new ArrayList<>(initializationDataSize);
- for (int i = 0; i < initializationDataSize; i++) {
- initializationData.add(in.createByteArray());
- }
- drmInitData = in.readParcelable(DrmInitData.class.getClassLoader());
- metadata = in.readParcelable(Metadata.class.getClassLoader());
+ // Provided by source.
+ exoMediaCryptoType = null;
}
public Format copyWithMaxInputSize(int maxInputSize) {
return new Format(
id,
label,
+ selectionFlags,
+ roleFlags,
+ bitrate,
+ codecs,
+ metadata,
containerMimeType,
sampleMimeType,
- codecs,
- bitrate,
maxInputSize,
+ initializationData,
+ drmInitData,
+ subsampleOffsetUs,
width,
height,
frameRate,
@@ -999,24 +1057,26 @@ public final class Format implements Parcelable {
pcmEncoding,
encoderDelay,
encoderPadding,
- selectionFlags,
language,
accessibilityChannel,
- subsampleOffsetUs,
- initializationData,
- drmInitData,
- metadata);
+ exoMediaCryptoType);
}
public Format copyWithSubsampleOffsetUs(long subsampleOffsetUs) {
return new Format(
id,
label,
+ selectionFlags,
+ roleFlags,
+ bitrate,
+ codecs,
+ metadata,
containerMimeType,
sampleMimeType,
- codecs,
- bitrate,
maxInputSize,
+ initializationData,
+ drmInitData,
+ subsampleOffsetUs,
width,
height,
frameRate,
@@ -1030,13 +1090,9 @@ public final class Format implements Parcelable {
pcmEncoding,
encoderDelay,
encoderPadding,
- selectionFlags,
language,
accessibilityChannel,
- subsampleOffsetUs,
- initializationData,
- drmInitData,
- metadata);
+ exoMediaCryptoType);
}
public Format copyWithContainerInfo(
@@ -1044,19 +1100,32 @@ public final class Format implements Parcelable {
@Nullable String label,
@Nullable String sampleMimeType,
@Nullable String codecs,
+ @Nullable Metadata metadata,
int bitrate,
int width,
int height,
+ int channelCount,
@C.SelectionFlags int selectionFlags,
@Nullable String language) {
+
+ if (this.metadata != null) {
+ metadata = this.metadata.copyWithAppendedEntriesFrom(metadata);
+ }
+
return new Format(
id,
label,
+ selectionFlags,
+ roleFlags,
+ bitrate,
+ codecs,
+ metadata,
containerMimeType,
sampleMimeType,
- codecs,
- bitrate,
maxInputSize,
+ initializationData,
+ drmInitData,
+ subsampleOffsetUs,
width,
height,
frameRate,
@@ -1070,13 +1139,9 @@ public final class Format implements Parcelable {
pcmEncoding,
encoderDelay,
encoderPadding,
- selectionFlags,
language,
accessibilityChannel,
- subsampleOffsetUs,
- initializationData,
- drmInitData,
- metadata);
+ exoMediaCryptoType);
}
@SuppressWarnings("ReferenceEquality")
@@ -1110,6 +1175,12 @@ public final class Format implements Parcelable {
codecs = codecsOfType;
}
}
+
+ Metadata metadata =
+ this.metadata == null
+ ? manifestFormat.metadata
+ : this.metadata.copyWithAppendedEntriesFrom(manifestFormat.metadata);
+
float frameRate = this.frameRate;
if (frameRate == NO_VALUE && trackType == C.TRACK_TYPE_VIDEO) {
frameRate = manifestFormat.frameRate;
@@ -1117,17 +1188,24 @@ public final class Format implements Parcelable {
// Merge manifest and sample format values.
@C.SelectionFlags int selectionFlags = this.selectionFlags | manifestFormat.selectionFlags;
+ @C.RoleFlags int roleFlags = this.roleFlags | manifestFormat.roleFlags;
DrmInitData drmInitData =
DrmInitData.createSessionCreationData(manifestFormat.drmInitData, this.drmInitData);
return new Format(
id,
label,
+ selectionFlags,
+ roleFlags,
+ bitrate,
+ codecs,
+ metadata,
containerMimeType,
sampleMimeType,
- codecs,
- bitrate,
maxInputSize,
+ initializationData,
+ drmInitData,
+ subsampleOffsetUs,
width,
height,
frameRate,
@@ -1141,24 +1219,26 @@ public final class Format implements Parcelable {
pcmEncoding,
encoderDelay,
encoderPadding,
- selectionFlags,
language,
accessibilityChannel,
- subsampleOffsetUs,
- initializationData,
- drmInitData,
- metadata);
+ exoMediaCryptoType);
}
public Format copyWithGaplessInfo(int encoderDelay, int encoderPadding) {
return new Format(
id,
label,
+ selectionFlags,
+ roleFlags,
+ bitrate,
+ codecs,
+ metadata,
containerMimeType,
sampleMimeType,
- codecs,
- bitrate,
maxInputSize,
+ initializationData,
+ drmInitData,
+ subsampleOffsetUs,
width,
height,
frameRate,
@@ -1172,55 +1252,72 @@ public final class Format implements Parcelable {
pcmEncoding,
encoderDelay,
encoderPadding,
- selectionFlags,
language,
accessibilityChannel,
- subsampleOffsetUs,
+ exoMediaCryptoType);
+ }
+
+ public Format copyWithFrameRate(float frameRate) {
+ return new Format(
+ id,
+ label,
+ selectionFlags,
+ roleFlags,
+ bitrate,
+ codecs,
+ metadata,
+ containerMimeType,
+ sampleMimeType,
+ maxInputSize,
initializationData,
drmInitData,
- metadata);
+ subsampleOffsetUs,
+ width,
+ height,
+ frameRate,
+ rotationDegrees,
+ pixelWidthHeightRatio,
+ projectionData,
+ stereoMode,
+ colorInfo,
+ channelCount,
+ sampleRate,
+ pcmEncoding,
+ encoderDelay,
+ encoderPadding,
+ language,
+ accessibilityChannel,
+ exoMediaCryptoType);
}
public Format copyWithDrmInitData(@Nullable DrmInitData drmInitData) {
- return new Format(
- id,
- label,
- containerMimeType,
- sampleMimeType,
- codecs,
- bitrate,
- maxInputSize,
- width,
- height,
- frameRate,
- rotationDegrees,
- pixelWidthHeightRatio,
- projectionData,
- stereoMode,
- colorInfo,
- channelCount,
- sampleRate,
- pcmEncoding,
- encoderDelay,
- encoderPadding,
- selectionFlags,
- language,
- accessibilityChannel,
- subsampleOffsetUs,
- initializationData,
- drmInitData,
- metadata);
+ return copyWithAdjustments(drmInitData, metadata);
}
public Format copyWithMetadata(@Nullable Metadata metadata) {
+ return copyWithAdjustments(drmInitData, metadata);
+ }
+
+ @SuppressWarnings("ReferenceEquality")
+ public Format copyWithAdjustments(
+ @Nullable DrmInitData drmInitData, @Nullable Metadata metadata) {
+ if (drmInitData == this.drmInitData && metadata == this.metadata) {
+ return this;
+ }
return new Format(
id,
label,
+ selectionFlags,
+ roleFlags,
+ bitrate,
+ codecs,
+ metadata,
containerMimeType,
sampleMimeType,
- codecs,
- bitrate,
maxInputSize,
+ initializationData,
+ drmInitData,
+ subsampleOffsetUs,
width,
height,
frameRate,
@@ -1234,24 +1331,26 @@ public final class Format implements Parcelable {
pcmEncoding,
encoderDelay,
encoderPadding,
- selectionFlags,
language,
accessibilityChannel,
- subsampleOffsetUs,
- initializationData,
- drmInitData,
- metadata);
+ exoMediaCryptoType);
}
public Format copyWithRotationDegrees(int rotationDegrees) {
return new Format(
id,
label,
+ selectionFlags,
+ roleFlags,
+ bitrate,
+ codecs,
+ metadata,
containerMimeType,
sampleMimeType,
- codecs,
- bitrate,
maxInputSize,
+ initializationData,
+ drmInitData,
+ subsampleOffsetUs,
width,
height,
frameRate,
@@ -1265,13 +1364,108 @@ public final class Format implements Parcelable {
pcmEncoding,
encoderDelay,
encoderPadding,
- selectionFlags,
language,
accessibilityChannel,
- subsampleOffsetUs,
+ exoMediaCryptoType);
+ }
+
+ public Format copyWithBitrate(int bitrate) {
+ return new Format(
+ id,
+ label,
+ selectionFlags,
+ roleFlags,
+ bitrate,
+ codecs,
+ metadata,
+ containerMimeType,
+ sampleMimeType,
+ maxInputSize,
initializationData,
drmInitData,
- metadata);
+ subsampleOffsetUs,
+ width,
+ height,
+ frameRate,
+ rotationDegrees,
+ pixelWidthHeightRatio,
+ projectionData,
+ stereoMode,
+ colorInfo,
+ channelCount,
+ sampleRate,
+ pcmEncoding,
+ encoderDelay,
+ encoderPadding,
+ language,
+ accessibilityChannel,
+ exoMediaCryptoType);
+ }
+
+ public Format copyWithVideoSize(int width, int height) {
+ return new Format(
+ id,
+ label,
+ selectionFlags,
+ roleFlags,
+ bitrate,
+ codecs,
+ metadata,
+ containerMimeType,
+ sampleMimeType,
+ maxInputSize,
+ initializationData,
+ drmInitData,
+ subsampleOffsetUs,
+ width,
+ height,
+ frameRate,
+ rotationDegrees,
+ pixelWidthHeightRatio,
+ projectionData,
+ stereoMode,
+ colorInfo,
+ channelCount,
+ sampleRate,
+ pcmEncoding,
+ encoderDelay,
+ encoderPadding,
+ language,
+ accessibilityChannel,
+ exoMediaCryptoType);
+ }
+
+ public Format copyWithExoMediaCryptoType(Class extends ExoMediaCrypto> exoMediaCryptoType) {
+ return new Format(
+ id,
+ label,
+ selectionFlags,
+ roleFlags,
+ bitrate,
+ codecs,
+ metadata,
+ containerMimeType,
+ sampleMimeType,
+ maxInputSize,
+ initializationData,
+ drmInitData,
+ subsampleOffsetUs,
+ width,
+ height,
+ frameRate,
+ rotationDegrees,
+ pixelWidthHeightRatio,
+ projectionData,
+ stereoMode,
+ colorInfo,
+ channelCount,
+ sampleRate,
+ pcmEncoding,
+ encoderDelay,
+ encoderPadding,
+ language,
+ accessibilityChannel,
+ exoMediaCryptoType);
}
/**
@@ -1315,32 +1509,43 @@ public final class Format implements Parcelable {
@Override
public int hashCode() {
if (hashCode == 0) {
+ // Some fields for which hashing is expensive are deliberately omitted.
int result = 17;
result = 31 * result + (id == null ? 0 : id.hashCode());
- result = 31 * result + (containerMimeType == null ? 0 : containerMimeType.hashCode());
- result = 31 * result + (sampleMimeType == null ? 0 : sampleMimeType.hashCode());
- result = 31 * result + (codecs == null ? 0 : codecs.hashCode());
+ result = 31 * result + (label != null ? label.hashCode() : 0);
+ result = 31 * result + selectionFlags;
+ result = 31 * result + roleFlags;
result = 31 * result + bitrate;
+ result = 31 * result + (codecs == null ? 0 : codecs.hashCode());
+ result = 31 * result + (metadata == null ? 0 : metadata.hashCode());
+ // Container specific.
+ result = 31 * result + (containerMimeType == null ? 0 : containerMimeType.hashCode());
+ // Elementary stream specific.
+ result = 31 * result + (sampleMimeType == null ? 0 : sampleMimeType.hashCode());
+ result = 31 * result + maxInputSize;
+ // [Omitted] initializationData.
+ // [Omitted] drmInitData.
+ result = 31 * result + (int) subsampleOffsetUs;
+ // Video specific.
result = 31 * result + width;
result = 31 * result + height;
+ result = 31 * result + Float.floatToIntBits(frameRate);
+ result = 31 * result + rotationDegrees;
+ result = 31 * result + Float.floatToIntBits(pixelWidthHeightRatio);
+ // [Omitted] projectionData.
+ result = 31 * result + stereoMode;
+ // [Omitted] colorInfo.
+ // Audio specific.
result = 31 * result + channelCount;
result = 31 * result + sampleRate;
- result = 31 * result + (language == null ? 0 : language.hashCode());
- result = 31 * result + accessibilityChannel;
- result = 31 * result + (drmInitData == null ? 0 : drmInitData.hashCode());
- result = 31 * result + (metadata == null ? 0 : metadata.hashCode());
- result = 31 * result + (label != null ? label.hashCode() : 0);
- result = 31 * result + maxInputSize;
- result = 31 * result + (int) subsampleOffsetUs;
- result = 31 * result + Float.floatToIntBits(frameRate);
- result = 31 * result + Float.floatToIntBits(pixelWidthHeightRatio);
- result = 31 * result + rotationDegrees;
- result = 31 * result + stereoMode;
result = 31 * result + pcmEncoding;
result = 31 * result + encoderDelay;
result = 31 * result + encoderPadding;
- result = 31 * result + selectionFlags;
- // Not all of the fields are included to keep the calculation quick enough.
+ // Audio and text specific.
+ result = 31 * result + (language == null ? 0 : language.hashCode());
+ result = 31 * result + accessibilityChannel;
+ // Provided by source.
+ result = 31 * result + (exoMediaCryptoType == null ? 0 : exoMediaCryptoType.hashCode());
hashCode = result;
}
return hashCode;
@@ -1358,32 +1563,35 @@ public final class Format implements Parcelable {
if (hashCode != 0 && other.hashCode != 0 && hashCode != other.hashCode) {
return false;
}
- return bitrate == other.bitrate
+ // Field equality checks ordered by type, with the cheapest checks first.
+ return selectionFlags == other.selectionFlags
+ && roleFlags == other.roleFlags
+ && bitrate == other.bitrate
&& maxInputSize == other.maxInputSize
+ && subsampleOffsetUs == other.subsampleOffsetUs
&& width == other.width
&& height == other.height
- && Float.compare(frameRate, other.frameRate) == 0
&& rotationDegrees == other.rotationDegrees
- && Float.compare(pixelWidthHeightRatio, other.pixelWidthHeightRatio) == 0
&& stereoMode == other.stereoMode
&& channelCount == other.channelCount
&& sampleRate == other.sampleRate
&& pcmEncoding == other.pcmEncoding
&& encoderDelay == other.encoderDelay
&& encoderPadding == other.encoderPadding
- && subsampleOffsetUs == other.subsampleOffsetUs
- && selectionFlags == other.selectionFlags
+ && accessibilityChannel == other.accessibilityChannel
+ && Float.compare(frameRate, other.frameRate) == 0
+ && Float.compare(pixelWidthHeightRatio, other.pixelWidthHeightRatio) == 0
+ && Util.areEqual(exoMediaCryptoType, other.exoMediaCryptoType)
&& Util.areEqual(id, other.id)
&& Util.areEqual(label, other.label)
- && Util.areEqual(language, other.language)
- && accessibilityChannel == other.accessibilityChannel
+ && Util.areEqual(codecs, other.codecs)
&& Util.areEqual(containerMimeType, other.containerMimeType)
&& Util.areEqual(sampleMimeType, other.sampleMimeType)
- && Util.areEqual(codecs, other.codecs)
- && Util.areEqual(drmInitData, other.drmInitData)
+ && Util.areEqual(language, other.language)
+ && Arrays.equals(projectionData, other.projectionData)
&& Util.areEqual(metadata, other.metadata)
&& Util.areEqual(colorInfo, other.colorInfo)
- && Arrays.equals(projectionData, other.projectionData)
+ && Util.areEqual(drmInitData, other.drmInitData)
&& initializationDataEquals(other);
}
@@ -1409,10 +1617,8 @@ public final class Format implements Parcelable {
// Utility methods
- /**
- * Returns a prettier {@link String} than {@link #toString()}, intended for logging.
- */
- public static String toLogString(Format format) {
+ /** Returns a prettier {@link String} than {@link #toString()}, intended for logging. */
+ public static String toLogString(@Nullable Format format) {
if (format == null) {
return "null";
}
@@ -1456,11 +1662,24 @@ public final class Format implements Parcelable {
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(id);
dest.writeString(label);
- dest.writeString(containerMimeType);
- dest.writeString(sampleMimeType);
- dest.writeString(codecs);
+ dest.writeInt(selectionFlags);
+ dest.writeInt(roleFlags);
dest.writeInt(bitrate);
+ dest.writeString(codecs);
+ dest.writeParcelable(metadata, 0);
+ // Container specific.
+ dest.writeString(containerMimeType);
+ // Elementary stream specific.
+ dest.writeString(sampleMimeType);
dest.writeInt(maxInputSize);
+ int initializationDataSize = initializationData.size();
+ dest.writeInt(initializationDataSize);
+ for (int i = 0; i < initializationDataSize; i++) {
+ dest.writeByteArray(initializationData.get(i));
+ }
+ dest.writeParcelable(drmInitData, 0);
+ dest.writeLong(subsampleOffsetUs);
+ // Video specific.
dest.writeInt(width);
dest.writeInt(height);
dest.writeFloat(frameRate);
@@ -1472,22 +1691,15 @@ public final class Format implements Parcelable {
}
dest.writeInt(stereoMode);
dest.writeParcelable(colorInfo, flags);
+ // Audio specific.
dest.writeInt(channelCount);
dest.writeInt(sampleRate);
dest.writeInt(pcmEncoding);
dest.writeInt(encoderDelay);
dest.writeInt(encoderPadding);
- dest.writeInt(selectionFlags);
+ // Audio and text specific.
dest.writeString(language);
dest.writeInt(accessibilityChannel);
- dest.writeLong(subsampleOffsetUs);
- int initializationDataSize = initializationData.size();
- dest.writeInt(initializationDataSize);
- for (int i = 0; i < initializationDataSize; i++) {
- dest.writeByteArray(initializationData.get(i));
- }
- dest.writeParcelable(drmInitData, 0);
- dest.writeParcelable(metadata, 0);
}
public static final Creator CREATOR = new Creator() {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/FormatHolder.java b/library/core/src/main/java/com/google/android/exoplayer2/FormatHolder.java
index 8c7ba1eb91..c1b2ace9b7 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/FormatHolder.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/FormatHolder.java
@@ -15,13 +15,22 @@
*/
package com.google.android.exoplayer2;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
+import com.google.android.exoplayer2.drm.DrmSession;
/**
* Holds a {@link Format}.
*/
public final class FormatHolder {
+ /** Whether the {@link #format} setter also sets the {@link #drmSession} field. */
+ // TODO: Remove once all Renderers and MediaSources have migrated to the new DRM model [Internal
+ // ref: b/129764794].
+ public boolean includesDrmSession;
+
+ /** An accompanying context for decrypting samples in the format. */
+ @Nullable public DrmSession> drmSession;
+
/** The held {@link Format}. */
- public @Nullable Format format;
+ @Nullable public Format format;
}
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 e42dd03dbe..850d2b7d10 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
@@ -15,11 +15,12 @@
*/
package com.google.android.exoplayer2;
-import android.util.Log;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.source.ClippingMediaPeriod;
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.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.source.SampleStream;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.TrackSelection;
@@ -28,41 +29,51 @@ 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;
+import com.google.android.exoplayer2.util.Log;
+import org.checkerframework.checker.nullness.compatqual.NullableType;
/** Holds a {@link MediaPeriod} with information required to play it as part of a timeline. */
/* package */ final class MediaPeriodHolder {
private static final String TAG = "MediaPeriodHolder";
+ /** The {@link MediaPeriod} wrapped by this class. */
public final MediaPeriod mediaPeriod;
+ /** The unique timeline period identifier the media period belongs to. */
public final Object uid;
- public final SampleStream[] sampleStreams;
- public final boolean[] mayRetainStreamFlags;
+ /**
+ * The sample streams for each renderer associated with this period. May contain null elements.
+ */
+ public final @NullableType SampleStream[] sampleStreams;
+ /** Whether the media period has finished preparing. */
public boolean prepared;
+ /** Whether any of the tracks of this media period are enabled. */
public boolean hasEnabledTracks;
+ /** {@link MediaPeriodInfo} about this media period. */
public MediaPeriodInfo info;
- public MediaPeriodHolder next;
- public TrackGroupArray trackGroups;
- public TrackSelectorResult trackSelectorResult;
+ private final boolean[] mayRetainStreamFlags;
private final RendererCapabilities[] rendererCapabilities;
private final TrackSelector trackSelector;
private final MediaSource mediaSource;
+ @Nullable private MediaPeriodHolder next;
+ private TrackGroupArray trackGroups;
+ private TrackSelectorResult trackSelectorResult;
private long rendererPositionOffsetUs;
- private TrackSelectorResult periodTrackSelectorResult;
/**
* Creates a new holder with information required to play it as part of a timeline.
*
* @param rendererCapabilities The renderer capabilities.
- * @param rendererPositionOffsetUs The time offset of the start of the media period to provide to
- * renderers.
+ * @param rendererPositionOffsetUs The renderer time of the start of the period, in microseconds.
* @param trackSelector The track selector.
* @param allocator The allocator.
* @param mediaSource The media source that produced the media period.
* @param info Information used to identify this media period in its timeline period.
+ * @param emptyTrackSelectorResult A {@link TrackSelectorResult} with empty selections for each
+ * renderer.
*/
public MediaPeriodHolder(
RendererCapabilities[] rendererCapabilities,
@@ -70,129 +81,202 @@ import com.google.android.exoplayer2.util.Assertions;
TrackSelector trackSelector,
Allocator allocator,
MediaSource mediaSource,
- MediaPeriodInfo info) {
+ MediaPeriodInfo info,
+ TrackSelectorResult emptyTrackSelectorResult) {
this.rendererCapabilities = rendererCapabilities;
- this.rendererPositionOffsetUs = rendererPositionOffsetUs - info.startPositionUs;
+ this.rendererPositionOffsetUs = rendererPositionOffsetUs;
this.trackSelector = trackSelector;
this.mediaSource = mediaSource;
- this.uid = Assertions.checkNotNull(info.id.periodUid);
+ this.uid = info.id.periodUid;
this.info = info;
+ this.trackGroups = TrackGroupArray.EMPTY;
+ this.trackSelectorResult = emptyTrackSelectorResult;
sampleStreams = new SampleStream[rendererCapabilities.length];
mayRetainStreamFlags = new boolean[rendererCapabilities.length];
- MediaPeriod mediaPeriod = mediaSource.createPeriod(info.id, allocator);
- if (info.id.endPositionUs != C.TIME_END_OF_SOURCE) {
- mediaPeriod =
- new ClippingMediaPeriod(
- mediaPeriod,
- /* enableInitialDiscontinuity= */ true,
- /* startUs= */ 0,
- info.id.endPositionUs);
- }
- this.mediaPeriod = mediaPeriod;
+ mediaPeriod =
+ createMediaPeriod(
+ info.id, mediaSource, allocator, info.startPositionUs, info.endPositionUs);
}
+ /**
+ * Converts time relative to the start of the period to the respective renderer time using {@link
+ * #getRendererOffset()}, in microseconds.
+ */
public long toRendererTime(long periodTimeUs) {
return periodTimeUs + getRendererOffset();
}
+ /**
+ * Converts renderer time to the respective time relative to the start of the period using {@link
+ * #getRendererOffset()}, in microseconds.
+ */
public long toPeriodTime(long rendererTimeUs) {
return rendererTimeUs - getRendererOffset();
}
+ /** Returns the renderer time of the start of the period, in microseconds. */
public long getRendererOffset() {
return rendererPositionOffsetUs;
}
+ /**
+ * Sets the renderer time of the start of the period, in microseconds.
+ *
+ * @param rendererPositionOffsetUs The new renderer position offset, in microseconds.
+ */
+ public void setRendererOffset(long rendererPositionOffsetUs) {
+ this.rendererPositionOffsetUs = rendererPositionOffsetUs;
+ }
+
+ /** Returns start position of period in renderer time. */
public long getStartPositionRendererTime() {
return info.startPositionUs + rendererPositionOffsetUs;
}
+ /** Returns whether the period is fully buffered. */
public boolean isFullyBuffered() {
return prepared
&& (!hasEnabledTracks || mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE);
}
- public long getDurationUs() {
- return info.durationUs;
- }
-
/**
- * Returns the buffered position in microseconds. If the period is buffered to the end then
- * {@link C#TIME_END_OF_SOURCE} is returned unless {@code convertEosToDuration} is true, in which
- * case the period duration is returned.
+ * Returns the buffered position in microseconds. If the period is buffered to the end, then the
+ * period duration is returned.
*
- * @param convertEosToDuration Whether to return the period duration rather than
- * {@link C#TIME_END_OF_SOURCE} if the period is fully buffered.
* @return The buffered position in microseconds.
*/
- public long getBufferedPositionUs(boolean convertEosToDuration) {
+ public long getBufferedPositionUs() {
if (!prepared) {
return info.startPositionUs;
}
long bufferedPositionUs =
hasEnabledTracks ? mediaPeriod.getBufferedPositionUs() : C.TIME_END_OF_SOURCE;
- return bufferedPositionUs == C.TIME_END_OF_SOURCE && convertEosToDuration
- ? info.durationUs
- : bufferedPositionUs;
+ return bufferedPositionUs == C.TIME_END_OF_SOURCE ? info.durationUs : bufferedPositionUs;
}
+ /**
+ * Returns the next load time relative to the start of the period, or {@link C#TIME_END_OF_SOURCE}
+ * if loading has finished.
+ */
public long getNextLoadPositionUs() {
return !prepared ? 0 : mediaPeriod.getNextLoadPositionUs();
}
- public void handlePrepared(float playbackSpeed) throws ExoPlaybackException {
+ /**
+ * Handles period preparation.
+ *
+ * @param playbackSpeed The current playback speed.
+ * @param timeline The current {@link Timeline}.
+ * @throws ExoPlaybackException If an error occurs during track selection.
+ */
+ public void handlePrepared(float playbackSpeed, Timeline timeline) throws ExoPlaybackException {
prepared = true;
trackGroups = mediaPeriod.getTrackGroups();
- selectTracks(playbackSpeed);
- long newStartPositionUs = applyTrackSelection(info.startPositionUs, false);
+ TrackSelectorResult selectorResult = selectTracks(playbackSpeed, timeline);
+ long newStartPositionUs =
+ applyTrackSelection(
+ selectorResult, info.startPositionUs, /* forceRecreateStreams= */ false);
rendererPositionOffsetUs += info.startPositionUs - newStartPositionUs;
info = info.copyWithStartPositionUs(newStartPositionUs);
}
+ /**
+ * Reevaluates the buffer of the media period at the given renderer position. Should only be
+ * called if this is the loading media period.
+ *
+ * @param rendererPositionUs The playing position in renderer time, in microseconds.
+ */
public void reevaluateBuffer(long rendererPositionUs) {
+ Assertions.checkState(isLoadingMediaPeriod());
if (prepared) {
mediaPeriod.reevaluateBuffer(toPeriodTime(rendererPositionUs));
}
}
+ /**
+ * Continues loading the media period at the given renderer position. Should only be called if
+ * this is the loading media period.
+ *
+ * @param rendererPositionUs The load position in renderer time, in microseconds.
+ */
public void continueLoading(long rendererPositionUs) {
+ Assertions.checkState(isLoadingMediaPeriod());
long loadingPeriodPositionUs = toPeriodTime(rendererPositionUs);
mediaPeriod.continueLoading(loadingPeriodPositionUs);
}
- public boolean selectTracks(float playbackSpeed) throws ExoPlaybackException {
+ /**
+ * Selects tracks for the period. Must only be called if {@link #prepared} is {@code true}.
+ *
+ * The new track selection needs to be applied with {@link
+ * #applyTrackSelection(TrackSelectorResult, long, boolean)} before taking effect.
+ *
+ * @param playbackSpeed The current playback speed.
+ * @param timeline The current {@link Timeline}.
+ * @return The {@link TrackSelectorResult}.
+ * @throws ExoPlaybackException If an error occurs during track selection.
+ */
+ public TrackSelectorResult selectTracks(float playbackSpeed, Timeline timeline)
+ throws ExoPlaybackException {
TrackSelectorResult selectorResult =
- trackSelector.selectTracks(rendererCapabilities, trackGroups);
- if (selectorResult.isEquivalent(periodTrackSelectorResult)) {
- return false;
- }
- trackSelectorResult = selectorResult;
- for (TrackSelection trackSelection : trackSelectorResult.selections.getAll()) {
+ trackSelector.selectTracks(rendererCapabilities, getTrackGroups(), info.id, timeline);
+ for (TrackSelection trackSelection : selectorResult.selections.getAll()) {
if (trackSelection != null) {
trackSelection.onPlaybackSpeed(playbackSpeed);
}
}
- return true;
- }
-
- public long applyTrackSelection(long positionUs, boolean forceRecreateStreams) {
- return applyTrackSelection(
- positionUs, forceRecreateStreams, new boolean[rendererCapabilities.length]);
+ return selectorResult;
}
+ /**
+ * Applies a {@link TrackSelectorResult} to the period.
+ *
+ * @param trackSelectorResult The {@link TrackSelectorResult} to apply.
+ * @param positionUs The position relative to the start of the period at which to apply the new
+ * track selections, in microseconds.
+ * @param forceRecreateStreams Whether all streams are forced to be recreated.
+ * @return The actual position relative to the start of the period at which the new track
+ * selections are applied.
+ */
public long applyTrackSelection(
- long positionUs, boolean forceRecreateStreams, boolean[] streamResetFlags) {
- for (int i = 0; i < trackSelectorResult.length; i++) {
+ TrackSelectorResult trackSelectorResult, long positionUs, boolean forceRecreateStreams) {
+ return applyTrackSelection(
+ trackSelectorResult,
+ positionUs,
+ forceRecreateStreams,
+ new boolean[rendererCapabilities.length]);
+ }
+
+ /**
+ * Applies a {@link TrackSelectorResult} to the period.
+ *
+ * @param newTrackSelectorResult The {@link TrackSelectorResult} to apply.
+ * @param positionUs The position relative to the start of the period at which to apply the new
+ * track selections, in microseconds.
+ * @param forceRecreateStreams Whether all streams are forced to be recreated.
+ * @param streamResetFlags Will be populated to indicate which streams have been reset or were
+ * newly created.
+ * @return The actual position relative to the start of the period at which the new track
+ * selections are applied.
+ */
+ public long applyTrackSelection(
+ TrackSelectorResult newTrackSelectorResult,
+ long positionUs,
+ boolean forceRecreateStreams,
+ boolean[] streamResetFlags) {
+ for (int i = 0; i < newTrackSelectorResult.length; i++) {
mayRetainStreamFlags[i] =
- !forceRecreateStreams && trackSelectorResult.isEquivalent(periodTrackSelectorResult, i);
+ !forceRecreateStreams && newTrackSelectorResult.isEquivalent(trackSelectorResult, i);
}
// Undo the effect of previous call to associate no-sample renderers with empty tracks
// so the mediaPeriod receives back whatever it sent us before.
disassociateNoSampleRenderersWithEmptySampleStream(sampleStreams);
- updatePeriodTrackSelectorResult(trackSelectorResult);
+ disableTrackSelectionsInResult();
+ trackSelectorResult = newTrackSelectorResult;
+ enableTrackSelectionsInResult();
// Disable streams on the period and get new streams for updated/newly-enabled tracks.
- TrackSelectionArray trackSelections = trackSelectorResult.selections;
+ TrackSelectionArray trackSelections = newTrackSelectorResult.selections;
positionUs =
mediaPeriod.selectTracks(
trackSelections.getAll(),
@@ -206,7 +290,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.isRendererEnabled(i));
+ Assertions.checkState(newTrackSelectorResult.isRendererEnabled(i));
// hasEnabledTracks should be true only when non-empty streams exists.
if (rendererCapabilities[i].getTrackType() != C.TRACK_TYPE_NONE) {
hasEnabledTracks = true;
@@ -218,31 +302,50 @@ import com.google.android.exoplayer2.util.Assertions;
return positionUs;
}
+ /** Releases the media period. No other method should be called after the release. */
public void release() {
- updatePeriodTrackSelectorResult(null);
- try {
- if (info.id.endPositionUs != C.TIME_END_OF_SOURCE) {
- mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod);
- } else {
- mediaSource.releasePeriod(mediaPeriod);
- }
- } catch (RuntimeException e) {
- // There's nothing we can do.
- Log.e(TAG, "Period release failed.", e);
- }
+ disableTrackSelectionsInResult();
+ releaseMediaPeriod(info.endPositionUs, mediaSource, mediaPeriod);
}
- private void updatePeriodTrackSelectorResult(TrackSelectorResult trackSelectorResult) {
- if (periodTrackSelectorResult != null) {
- disableTrackSelectionsInResult(periodTrackSelectorResult);
- }
- periodTrackSelectorResult = trackSelectorResult;
- if (periodTrackSelectorResult != null) {
- enableTrackSelectionsInResult(periodTrackSelectorResult);
+ /**
+ * Sets the next media period holder in the queue.
+ *
+ * @param nextMediaPeriodHolder The next holder, or null if this will be the new loading media
+ * period holder at the end of the queue.
+ */
+ public void setNext(@Nullable MediaPeriodHolder nextMediaPeriodHolder) {
+ if (nextMediaPeriodHolder == next) {
+ return;
}
+ disableTrackSelectionsInResult();
+ next = nextMediaPeriodHolder;
+ enableTrackSelectionsInResult();
}
- private void enableTrackSelectionsInResult(TrackSelectorResult trackSelectorResult) {
+ /**
+ * Returns the next media period holder in the queue, or null if this is the last media period
+ * (and thus the loading media period).
+ */
+ @Nullable
+ public MediaPeriodHolder getNext() {
+ return next;
+ }
+
+ /** Returns the {@link TrackGroupArray} exposed by this media period. */
+ public TrackGroupArray getTrackGroups() {
+ return trackGroups;
+ }
+
+ /** Returns the {@link TrackSelectorResult} which is currently applied. */
+ public TrackSelectorResult getTrackSelectorResult() {
+ return trackSelectorResult;
+ }
+
+ private void enableTrackSelectionsInResult() {
+ if (!isLoadingMediaPeriod()) {
+ return;
+ }
for (int i = 0; i < trackSelectorResult.length; i++) {
boolean rendererEnabled = trackSelectorResult.isRendererEnabled(i);
TrackSelection trackSelection = trackSelectorResult.selections.get(i);
@@ -252,7 +355,10 @@ import com.google.android.exoplayer2.util.Assertions;
}
}
- private void disableTrackSelectionsInResult(TrackSelectorResult trackSelectorResult) {
+ private void disableTrackSelectionsInResult() {
+ if (!isLoadingMediaPeriod()) {
+ return;
+ }
for (int i = 0; i < trackSelectorResult.length; i++) {
boolean rendererEnabled = trackSelectorResult.isRendererEnabled(i);
TrackSelection trackSelection = trackSelectorResult.selections.get(i);
@@ -266,7 +372,8 @@ import com.google.android.exoplayer2.util.Assertions;
* For each renderer of type {@link C#TRACK_TYPE_NONE}, we will remove the dummy {@link
* EmptySampleStream} that was associated with it.
*/
- private void disassociateNoSampleRenderersWithEmptySampleStream(SampleStream[] sampleStreams) {
+ private void disassociateNoSampleRenderersWithEmptySampleStream(
+ @NullableType SampleStream[] sampleStreams) {
for (int i = 0; i < rendererCapabilities.length; i++) {
if (rendererCapabilities[i].getTrackType() == C.TRACK_TYPE_NONE) {
sampleStreams[i] = null;
@@ -278,7 +385,8 @@ import com.google.android.exoplayer2.util.Assertions;
* For each renderer of type {@link C#TRACK_TYPE_NONE} that was enabled, we will associate it with
* a dummy {@link EmptySampleStream}.
*/
- private void associateNoSampleRenderersWithEmptySampleStream(SampleStream[] sampleStreams) {
+ private void associateNoSampleRenderersWithEmptySampleStream(
+ @NullableType SampleStream[] sampleStreams) {
for (int i = 0; i < rendererCapabilities.length; i++) {
if (rendererCapabilities[i].getTrackType() == C.TRACK_TYPE_NONE
&& trackSelectorResult.isRendererEnabled(i)) {
@@ -286,4 +394,39 @@ import com.google.android.exoplayer2.util.Assertions;
}
}
}
+
+ private boolean isLoadingMediaPeriod() {
+ return next == null;
+ }
+
+ /** Returns a media period corresponding to the given {@code id}. */
+ private static MediaPeriod createMediaPeriod(
+ MediaPeriodId id,
+ MediaSource mediaSource,
+ Allocator allocator,
+ long startPositionUs,
+ long endPositionUs) {
+ MediaPeriod mediaPeriod = mediaSource.createPeriod(id, allocator, startPositionUs);
+ if (endPositionUs != C.TIME_UNSET && endPositionUs != C.TIME_END_OF_SOURCE) {
+ mediaPeriod =
+ new ClippingMediaPeriod(
+ mediaPeriod, /* enableInitialDiscontinuity= */ true, /* startUs= */ 0, endPositionUs);
+ }
+ return mediaPeriod;
+ }
+
+ /** Releases the given {@code mediaPeriod}, logging and suppressing any errors. */
+ private static void releaseMediaPeriod(
+ long endPositionUs, MediaSource mediaSource, MediaPeriod mediaPeriod) {
+ try {
+ if (endPositionUs != C.TIME_UNSET && endPositionUs != C.TIME_END_OF_SOURCE) {
+ mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod);
+ } else {
+ mediaSource.releasePeriod(mediaPeriod);
+ }
+ } catch (RuntimeException e) {
+ // There's nothing we can do.
+ Log.e(TAG, "Period release failed.", e);
+ }
+ }
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java
index ba19b54c3f..2733df7ba6 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java
@@ -15,8 +15,10 @@
*/
package com.google.android.exoplayer2;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
+import com.google.android.exoplayer2.util.Util;
/** Stores the information required to load and play a {@link MediaPeriod}. */
/* package */ final class MediaPeriodInfo {
@@ -27,13 +29,21 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
public final long startPositionUs;
/**
* If this is an ad, the position to play in the next content media period. {@link C#TIME_UNSET}
- * otherwise.
+ * if this is not an ad or the next content media period should be played from its default
+ * position.
*/
public final long contentPositionUs;
/**
- * The duration of the media period, like {@link MediaPeriodId#endPositionUs} but with {@link
- * C#TIME_END_OF_SOURCE} resolved to the timeline period duration. May be {@link C#TIME_UNSET} if
- * the end position is not known.
+ * The end position to which the media period's content is clipped in order to play a following ad
+ * group, in microseconds, or {@link C#TIME_UNSET} if there is no following ad group or if this
+ * media period is an ad. The value {@link C#TIME_END_OF_SOURCE} indicates that a postroll ad
+ * follows at the end of this content media period.
+ */
+ public final long endPositionUs;
+ /**
+ * The duration of the media period, like {@link #endPositionUs} but with {@link
+ * C#TIME_END_OF_SOURCE} and {@link C#TIME_UNSET} resolved to the timeline period duration if
+ * known.
*/
public final long durationUs;
/**
@@ -51,25 +61,81 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
MediaPeriodId id,
long startPositionUs,
long contentPositionUs,
+ long endPositionUs,
long durationUs,
boolean isLastInTimelinePeriod,
boolean isFinal) {
this.id = id;
this.startPositionUs = startPositionUs;
this.contentPositionUs = contentPositionUs;
+ this.endPositionUs = endPositionUs;
this.durationUs = durationUs;
this.isLastInTimelinePeriod = isLastInTimelinePeriod;
this.isFinal = isFinal;
}
- /** Returns a copy of this instance with the start position set to the specified value. */
+ /**
+ * Returns a copy of this instance with the start position set to the specified value. May return
+ * the same instance if nothing changed.
+ */
public MediaPeriodInfo copyWithStartPositionUs(long startPositionUs) {
- return new MediaPeriodInfo(
- id,
- startPositionUs,
- contentPositionUs,
- durationUs,
- isLastInTimelinePeriod,
- isFinal);
+ return startPositionUs == this.startPositionUs
+ ? this
+ : new MediaPeriodInfo(
+ id,
+ startPositionUs,
+ contentPositionUs,
+ endPositionUs,
+ durationUs,
+ isLastInTimelinePeriod,
+ isFinal);
+ }
+
+ /**
+ * Returns a copy of this instance with the content position set to the specified value. May
+ * return the same instance if nothing changed.
+ */
+ public MediaPeriodInfo copyWithContentPositionUs(long contentPositionUs) {
+ return contentPositionUs == this.contentPositionUs
+ ? this
+ : new MediaPeriodInfo(
+ id,
+ startPositionUs,
+ contentPositionUs,
+ endPositionUs,
+ durationUs,
+ isLastInTimelinePeriod,
+ isFinal);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ MediaPeriodInfo that = (MediaPeriodInfo) o;
+ return startPositionUs == that.startPositionUs
+ && contentPositionUs == that.contentPositionUs
+ && endPositionUs == that.endPositionUs
+ && durationUs == that.durationUs
+ && isLastInTimelinePeriod == that.isLastInTimelinePeriod
+ && isFinal == that.isFinal
+ && Util.areEqual(id, that.id);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + id.hashCode();
+ result = 31 * result + (int) startPositionUs;
+ result = 31 * result + (int) contentPositionUs;
+ result = 31 * result + (int) endPositionUs;
+ result = 31 * result + (int) durationUs;
+ result = 31 * result + (isLastInTimelinePeriod ? 1 : 0);
+ result = 31 * result + (isFinal ? 1 : 0);
+ return result;
}
}
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 2edf7bb8c6..0f279ba6d3 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
@@ -15,13 +15,14 @@
*/
package com.google.android.exoplayer2;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
import android.util.Pair;
import com.google.android.exoplayer2.Player.RepeatMode;
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;
@@ -46,11 +47,11 @@ import com.google.android.exoplayer2.util.Assertions;
private Timeline timeline;
private @RepeatMode int repeatMode;
private boolean shuffleModeEnabled;
- private @Nullable MediaPeriodHolder playing;
- private @Nullable MediaPeriodHolder reading;
- private @Nullable MediaPeriodHolder loading;
+ @Nullable private MediaPeriodHolder playing;
+ @Nullable private MediaPeriodHolder reading;
+ @Nullable private MediaPeriodHolder loading;
private int length;
- private @Nullable Object oldFrontPeriodUid;
+ @Nullable private Object oldFrontPeriodUid;
private long oldFrontPeriodWindowSequenceNumber;
/** Creates a new media period queue. */
@@ -61,8 +62,8 @@ import com.google.android.exoplayer2.util.Assertions;
}
/**
- * Sets the {@link Timeline}. Call {@link #updateQueuedPeriods(MediaPeriodId, long)} to update the
- * queued media periods to take into account the new timeline.
+ * Sets the {@link Timeline}. Call {@link #updateQueuedPeriods(long, long)} to update the queued
+ * media periods to take into account the new timeline.
*/
public void setTimeline(Timeline timeline) {
this.timeline = timeline;
@@ -135,17 +136,22 @@ import com.google.android.exoplayer2.util.Assertions;
* @param allocator The allocator.
* @param mediaSource The media source that produced the media period.
* @param info Information used to identify this media period in its timeline period.
+ * @param emptyTrackSelectorResult A {@link TrackSelectorResult} with empty selections for each
+ * renderer.
*/
public MediaPeriod enqueueNextMediaPeriod(
RendererCapabilities[] rendererCapabilities,
TrackSelector trackSelector,
Allocator allocator,
MediaSource mediaSource,
- MediaPeriodInfo info) {
+ MediaPeriodInfo info,
+ TrackSelectorResult emptyTrackSelectorResult) {
long rendererPositionOffsetUs =
loading == null
- ? info.startPositionUs
- : (loading.getRendererOffset() + loading.info.durationUs);
+ ? (info.id.isAd() && info.contentPositionUs != C.TIME_UNSET
+ ? info.contentPositionUs
+ : 0)
+ : (loading.getRendererOffset() + loading.info.durationUs - info.startPositionUs);
MediaPeriodHolder newPeriodHolder =
new MediaPeriodHolder(
rendererCapabilities,
@@ -153,10 +159,11 @@ import com.google.android.exoplayer2.util.Assertions;
trackSelector,
allocator,
mediaSource,
- info);
+ info,
+ emptyTrackSelectorResult);
if (loading != null) {
Assertions.checkState(hasPlayingPeriod());
- loading.next = newPeriodHolder;
+ loading.setNext(newPeriodHolder);
}
oldFrontPeriodUid = null;
loading = newPeriodHolder;
@@ -168,6 +175,7 @@ import com.google.android.exoplayer2.util.Assertions;
* Returns the loading period holder which is at the end of the queue, or null if the queue is
* empty.
*/
+ @Nullable
public MediaPeriodHolder getLoadingPeriod() {
return loading;
}
@@ -176,6 +184,7 @@ import com.google.android.exoplayer2.util.Assertions;
* Returns the playing period holder which is at the front of the queue, or null if the queue is
* empty or hasn't started playing.
*/
+ @Nullable
public MediaPeriodHolder getPlayingPeriod() {
return playing;
}
@@ -184,6 +193,7 @@ import com.google.android.exoplayer2.util.Assertions;
* Returns the reading period holder, or null if the queue is empty or the player hasn't started
* reading.
*/
+ @Nullable
public MediaPeriodHolder getReadingPeriod() {
return reading;
}
@@ -192,6 +202,7 @@ import com.google.android.exoplayer2.util.Assertions;
* Returns the period holder in the front of the queue which is the playing period holder when
* playing, or null if the queue is empty.
*/
+ @Nullable
public MediaPeriodHolder getFrontPeriod() {
return hasPlayingPeriod() ? playing : loading;
}
@@ -207,8 +218,8 @@ import com.google.android.exoplayer2.util.Assertions;
* @return The updated reading period holder.
*/
public MediaPeriodHolder advanceReadingPeriod() {
- Assertions.checkState(reading != null && reading.next != null);
- reading = reading.next;
+ Assertions.checkState(reading != null && reading.getNext() != null);
+ reading = reading.getNext();
return reading;
}
@@ -219,10 +230,11 @@ import com.google.android.exoplayer2.util.Assertions;
*
* @return The updated playing period holder, or null if the queue is or becomes empty.
*/
+ @Nullable
public MediaPeriodHolder advancePlayingPeriod() {
if (playing != null) {
if (playing == reading) {
- reading = playing.next;
+ reading = playing.getNext();
}
playing.release();
length--;
@@ -231,7 +243,7 @@ import com.google.android.exoplayer2.util.Assertions;
oldFrontPeriodUid = playing.uid;
oldFrontPeriodWindowSequenceNumber = playing.info.id.windowSequenceNumber;
}
- playing = playing.next;
+ playing = playing.getNext();
} else {
playing = loading;
reading = loading;
@@ -251,8 +263,8 @@ import com.google.android.exoplayer2.util.Assertions;
Assertions.checkState(mediaPeriodHolder != null);
boolean removedReading = false;
loading = mediaPeriodHolder;
- while (mediaPeriodHolder.next != null) {
- mediaPeriodHolder = mediaPeriodHolder.next;
+ while (mediaPeriodHolder.getNext() != null) {
+ mediaPeriodHolder = mediaPeriodHolder.getNext();
if (mediaPeriodHolder == reading) {
reading = playing;
removedReading = true;
@@ -260,7 +272,7 @@ import com.google.android.exoplayer2.util.Assertions;
mediaPeriodHolder.release();
length--;
}
- loading.next = null;
+ loading.setNext(null);
return removedReading;
}
@@ -275,8 +287,8 @@ import com.google.android.exoplayer2.util.Assertions;
if (front != null) {
oldFrontPeriodUid = keepFrontPeriodUid ? front.uid : null;
oldFrontPeriodWindowSequenceNumber = front.info.id.windowSequenceNumber;
- front.release();
removeAfter(front);
+ front.release();
} else if (!keepFrontPeriodUid) {
oldFrontPeriodUid = null;
}
@@ -292,52 +304,60 @@ import com.google.android.exoplayer2.util.Assertions;
* current playback position. The method assumes that the first media period in the queue is still
* consistent with the new timeline.
*
- * @param playingPeriodId The current playing media period identifier.
* @param rendererPositionUs The current renderer position in microseconds.
+ * @param maxRendererReadPositionUs The maximum renderer position up to which renderers have read
+ * the current reading media period in microseconds, or {@link C#TIME_END_OF_SOURCE} if they
+ * have read to the end.
* @return Whether the timeline change has been handled completely.
*/
- public boolean updateQueuedPeriods(MediaPeriodId playingPeriodId, long rendererPositionUs) {
+ public boolean updateQueuedPeriods(long rendererPositionUs, long maxRendererReadPositionUs) {
// TODO: Merge this into setTimeline so that the queue gets updated as soon as the new timeline
// is set, once all cases handled by ExoPlayerImplInternal.handleSourceInfoRefreshed can be
// handled here.
- int periodIndex = timeline.getIndexOfPeriod(playingPeriodId.periodUid);
- // The front period is either playing now, or is being loaded and will become the playing
- // period.
MediaPeriodHolder previousPeriodHolder = null;
MediaPeriodHolder periodHolder = getFrontPeriod();
while (periodHolder != null) {
+ MediaPeriodInfo oldPeriodInfo = periodHolder.info;
+
+ // Get period info based on new timeline.
+ MediaPeriodInfo newPeriodInfo;
if (previousPeriodHolder == null) {
- periodHolder.info = getUpdatedMediaPeriodInfo(periodHolder.info);
+ // The id and start position of the first period have already been verified by
+ // ExoPlayerImplInternal.handleSourceInfoRefreshed. Just update duration, isLastInTimeline
+ // and isLastInPeriod flags.
+ newPeriodInfo = getUpdatedMediaPeriodInfo(oldPeriodInfo);
} else {
- // Check this period holder still follows the previous one, based on the new timeline.
- if (periodIndex == C.INDEX_UNSET
- || !periodHolder.uid.equals(timeline.getUidOfPeriod(periodIndex))) {
- // The holder uid is inconsistent with the new timeline.
- return !removeAfter(previousPeriodHolder);
- }
- MediaPeriodInfo periodInfo =
- getFollowingMediaPeriodInfo(previousPeriodHolder, rendererPositionUs);
- if (periodInfo == null) {
+ newPeriodInfo = getFollowingMediaPeriodInfo(previousPeriodHolder, rendererPositionUs);
+ if (newPeriodInfo == null) {
// We've loaded a next media period that is not in the new timeline.
return !removeAfter(previousPeriodHolder);
}
- // Update the period holder.
- periodHolder.info = getUpdatedMediaPeriodInfo(periodHolder.info);
- // Check the media period information matches the new timeline.
- if (!canKeepMediaPeriodHolder(periodHolder, periodInfo)) {
+ if (!canKeepMediaPeriodHolder(oldPeriodInfo, newPeriodInfo)) {
+ // The new media period has a different id or start position.
return !removeAfter(previousPeriodHolder);
}
}
- if (periodHolder.info.isLastInTimelinePeriod) {
- // Move on to the next timeline period index, if there is one.
- periodIndex =
- timeline.getNextPeriodIndex(
- periodIndex, period, window, repeatMode, shuffleModeEnabled);
+ // Use new period info, but keep old content position.
+ periodHolder.info = newPeriodInfo.copyWithContentPositionUs(oldPeriodInfo.contentPositionUs);
+
+ if (!areDurationsCompatible(oldPeriodInfo.durationUs, newPeriodInfo.durationUs)) {
+ // The period duration changed. Remove all subsequent periods and check whether we read
+ // beyond the new duration.
+ long newDurationInRendererTime =
+ newPeriodInfo.durationUs == C.TIME_UNSET
+ ? Long.MAX_VALUE
+ : periodHolder.toRendererTime(newPeriodInfo.durationUs);
+ boolean isReadingAndReadBeyondNewDuration =
+ periodHolder == reading
+ && (maxRendererReadPositionUs == C.TIME_END_OF_SOURCE
+ || maxRendererReadPositionUs >= newDurationInRendererTime);
+ boolean readingPeriodRemoved = removeAfter(periodHolder);
+ return !readingPeriodRemoved && !isReadingAndReadBeyondNewDuration;
}
previousPeriodHolder = periodHolder;
- periodHolder = periodHolder.next;
+ periodHolder = periodHolder.getNext();
}
return true;
}
@@ -351,19 +371,21 @@ import com.google.android.exoplayer2.util.Assertions;
* @return The updated media period info for the current timeline.
*/
public MediaPeriodInfo getUpdatedMediaPeriodInfo(MediaPeriodInfo info) {
- boolean isLastInPeriod = isLastInPeriod(info.id);
- boolean isLastInTimeline = isLastInTimeline(info.id, isLastInPeriod);
+ MediaPeriodId id = info.id;
+ boolean isLastInPeriod = isLastInPeriod(id);
+ boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod);
timeline.getPeriodByUid(info.id.periodUid, period);
long durationUs =
- info.id.isAd()
- ? period.getAdDurationUs(info.id.adGroupIndex, info.id.adIndexInAdGroup)
- : (info.id.endPositionUs == C.TIME_END_OF_SOURCE
+ id.isAd()
+ ? period.getAdDurationUs(id.adGroupIndex, id.adIndexInAdGroup)
+ : (info.endPositionUs == C.TIME_UNSET || info.endPositionUs == C.TIME_END_OF_SOURCE
? period.getDurationUs()
- : info.id.endPositionUs);
+ : info.endPositionUs);
return new MediaPeriodInfo(
- info.id,
+ id,
info.startPositionUs,
info.contentPositionUs,
+ info.endPositionUs,
durationUs,
isLastInPeriod,
isLastInTimeline);
@@ -402,11 +424,7 @@ import com.google.android.exoplayer2.util.Assertions;
int adGroupIndex = period.getAdGroupIndexForPositionUs(positionUs);
if (adGroupIndex == C.INDEX_UNSET) {
int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(positionUs);
- long endPositionUs =
- nextAdGroupIndex == C.INDEX_UNSET
- ? C.TIME_END_OF_SOURCE
- : period.getAdGroupTimeUs(nextAdGroupIndex);
- return new MediaPeriodId(periodUid, windowSequenceNumber, endPositionUs);
+ return new MediaPeriodId(periodUid, windowSequenceNumber, nextAdGroupIndex);
} else {
int adIndexInAdGroup = period.getFirstAdIndexToPlay(adGroupIndex);
return new MediaPeriodId(periodUid, adGroupIndex, adIndexInAdGroup, windowSequenceNumber);
@@ -439,7 +457,7 @@ import com.google.android.exoplayer2.util.Assertions;
// Reuse window sequence number of first exact period match.
return mediaPeriodHolder.info.id.windowSequenceNumber;
}
- mediaPeriodHolder = mediaPeriodHolder.next;
+ mediaPeriodHolder = mediaPeriodHolder.getNext();
}
mediaPeriodHolder = getFrontPeriod();
while (mediaPeriodHolder != null) {
@@ -451,20 +469,25 @@ import com.google.android.exoplayer2.util.Assertions;
return mediaPeriodHolder.info.id.windowSequenceNumber;
}
}
- mediaPeriodHolder = mediaPeriodHolder.next;
+ mediaPeriodHolder = mediaPeriodHolder.getNext();
}
// If no match is found, create new sequence number.
return nextWindowSequenceNumber++;
}
/**
- * Returns whether {@code periodHolder} can be kept for playing the media period described by
- * {@code info}.
+ * Returns whether a period described by {@code oldInfo} can be kept for playing the media period
+ * described by {@code newInfo}.
*/
- private boolean canKeepMediaPeriodHolder(MediaPeriodHolder periodHolder, MediaPeriodInfo info) {
- MediaPeriodInfo periodHolderInfo = periodHolder.info;
- return periodHolderInfo.startPositionUs == info.startPositionUs
- && periodHolderInfo.id.equals(info.id);
+ private boolean canKeepMediaPeriodHolder(MediaPeriodInfo oldInfo, MediaPeriodInfo newInfo) {
+ return oldInfo.startPositionUs == newInfo.startPositionUs && oldInfo.id.equals(newInfo.id);
+ }
+
+ /**
+ * Returns whether a duration change of a period is compatible with keeping the following periods.
+ */
+ private boolean areDurationsCompatible(long previousDurationUs, long newDurationUs) {
+ return previousDurationUs == C.TIME_UNSET || previousDurationUs == newDurationUs;
}
/**
@@ -482,19 +505,20 @@ import com.google.android.exoplayer2.util.Assertions;
int nextPeriodIndex =
timeline.getNextPeriodIndex(
currentPeriodIndex, period, window, repeatMode, shuffleModeEnabled);
- while (lastValidPeriodHolder.next != null
+ while (lastValidPeriodHolder.getNext() != null
&& !lastValidPeriodHolder.info.isLastInTimelinePeriod) {
- lastValidPeriodHolder = lastValidPeriodHolder.next;
+ lastValidPeriodHolder = lastValidPeriodHolder.getNext();
}
- if (nextPeriodIndex == C.INDEX_UNSET || lastValidPeriodHolder.next == null) {
+ MediaPeriodHolder nextMediaPeriodHolder = lastValidPeriodHolder.getNext();
+ if (nextPeriodIndex == C.INDEX_UNSET || nextMediaPeriodHolder == null) {
break;
}
- int nextPeriodHolderPeriodIndex = timeline.getIndexOfPeriod(lastValidPeriodHolder.next.uid);
+ int nextPeriodHolderPeriodIndex = timeline.getIndexOfPeriod(nextMediaPeriodHolder.uid);
if (nextPeriodHolderPeriodIndex != nextPeriodIndex) {
break;
}
- lastValidPeriodHolder = lastValidPeriodHolder.next;
+ lastValidPeriodHolder = nextMediaPeriodHolder;
currentPeriodIndex = nextPeriodIndex;
}
@@ -532,6 +556,11 @@ import com.google.android.exoplayer2.util.Assertions;
// until the timeline is updated. Store whether the next timeline period is ready when the
// timeline is updated, to avoid repeatedly checking the same timeline.
MediaPeriodInfo mediaPeriodInfo = mediaPeriodHolder.info;
+ // The expected delay until playback transitions to the new period is equal the duration of
+ // media that's currently buffered (assuming no interruptions). This is used to project forward
+ // the start position for transitions to new windows.
+ long bufferedDurationUs =
+ mediaPeriodHolder.getRendererOffset() + mediaPeriodInfo.durationUs - rendererPositionUs;
if (mediaPeriodInfo.isLastInTimelinePeriod) {
int currentPeriodIndex = timeline.getIndexOfPeriod(mediaPeriodInfo.id.periodUid);
int nextPeriodIndex =
@@ -543,41 +572,42 @@ import com.google.android.exoplayer2.util.Assertions;
}
long startPositionUs;
+ long contentPositionUs;
int nextWindowIndex =
timeline.getPeriod(nextPeriodIndex, period, /* setIds= */ true).windowIndex;
Object nextPeriodUid = period.uid;
long windowSequenceNumber = mediaPeriodInfo.id.windowSequenceNumber;
if (timeline.getWindow(nextWindowIndex, window).firstPeriodIndex == nextPeriodIndex) {
// We're starting to buffer a new window. When playback transitions to this window we'll
- // want it to be from its default start position. The expected delay until playback
- // transitions is equal the duration of media that's currently buffered (assuming no
- // interruptions). Hence we project the default start position forward by the duration of
- // the buffer, and start buffering from this point.
- long defaultPositionProjectionUs =
- mediaPeriodHolder.getRendererOffset() + mediaPeriodInfo.durationUs - rendererPositionUs;
+ // want it to be from its default start position, so project the default start position
+ // forward by the duration of the buffer, and start buffering from this point.
+ contentPositionUs = C.TIME_UNSET;
Pair defaultPosition =
timeline.getPeriodPosition(
window,
period,
nextWindowIndex,
- C.TIME_UNSET,
- Math.max(0, defaultPositionProjectionUs));
+ /* windowPositionUs= */ C.TIME_UNSET,
+ /* defaultPositionProjectionUs= */ Math.max(0, bufferedDurationUs));
if (defaultPosition == null) {
return null;
}
nextPeriodUid = defaultPosition.first;
startPositionUs = defaultPosition.second;
- if (mediaPeriodHolder.next != null && mediaPeriodHolder.next.uid.equals(nextPeriodUid)) {
- windowSequenceNumber = mediaPeriodHolder.next.info.id.windowSequenceNumber;
+ MediaPeriodHolder nextMediaPeriodHolder = mediaPeriodHolder.getNext();
+ if (nextMediaPeriodHolder != null && nextMediaPeriodHolder.uid.equals(nextPeriodUid)) {
+ windowSequenceNumber = nextMediaPeriodHolder.info.id.windowSequenceNumber;
} else {
windowSequenceNumber = nextWindowSequenceNumber++;
}
} else {
+ // We're starting to buffer a new period within the same window.
startPositionUs = 0;
+ contentPositionUs = 0;
}
MediaPeriodId periodId =
resolveMediaPeriodIdForAds(nextPeriodUid, startPositionUs, windowSequenceNumber);
- return getMediaPeriodInfo(periodId, startPositionUs, startPositionUs);
+ return getMediaPeriodInfo(periodId, contentPositionUs, startPositionUs);
}
MediaPeriodId currentPeriodId = mediaPeriodInfo.id;
@@ -602,19 +632,33 @@ import com.google.android.exoplayer2.util.Assertions;
currentPeriodId.windowSequenceNumber);
} else {
// Play content from the ad group position.
+ long startPositionUs = mediaPeriodInfo.contentPositionUs;
+ if (startPositionUs == C.TIME_UNSET) {
+ // If we're transitioning from an ad group to content starting from its default position,
+ // project the start position forward as if this were a transition to a new window.
+ Pair defaultPosition =
+ timeline.getPeriodPosition(
+ window,
+ period,
+ period.windowIndex,
+ /* windowPositionUs= */ C.TIME_UNSET,
+ /* defaultPositionProjectionUs= */ Math.max(0, bufferedDurationUs));
+ if (defaultPosition == null) {
+ return null;
+ }
+ startPositionUs = defaultPosition.second;
+ }
return getMediaPeriodInfoForContent(
- currentPeriodId.periodUid,
- mediaPeriodInfo.contentPositionUs,
- currentPeriodId.windowSequenceNumber);
+ currentPeriodId.periodUid, startPositionUs, currentPeriodId.windowSequenceNumber);
}
- } else if (mediaPeriodInfo.id.endPositionUs != C.TIME_END_OF_SOURCE) {
+ } else {
// Play the next ad group if it's available.
- int nextAdGroupIndex = period.getAdGroupIndexForPositionUs(mediaPeriodInfo.id.endPositionUs);
+ int nextAdGroupIndex = period.getAdGroupIndexForPositionUs(mediaPeriodInfo.endPositionUs);
if (nextAdGroupIndex == C.INDEX_UNSET) {
- // The next ad group can't be played. Play content from the ad group position instead.
+ // The next ad group can't be played. Play content from the previous end position instead.
return getMediaPeriodInfoForContent(
currentPeriodId.periodUid,
- mediaPeriodInfo.id.endPositionUs,
+ /* startPositionUs= */ mediaPeriodInfo.durationUs,
currentPeriodId.windowSequenceNumber);
}
int adIndexInAdGroup = period.getFirstAdIndexToPlay(nextAdGroupIndex);
@@ -624,30 +668,8 @@ import com.google.android.exoplayer2.util.Assertions;
currentPeriodId.periodUid,
nextAdGroupIndex,
adIndexInAdGroup,
- mediaPeriodInfo.id.endPositionUs,
+ /* contentPositionUs= */ mediaPeriodInfo.durationUs,
currentPeriodId.windowSequenceNumber);
- } else {
- // Check if the postroll ad should be played.
- int adGroupCount = period.getAdGroupCount();
- if (adGroupCount == 0) {
- return null;
- }
- int adGroupIndex = adGroupCount - 1;
- if (period.getAdGroupTimeUs(adGroupIndex) != C.TIME_END_OF_SOURCE
- || period.hasPlayedAdGroup(adGroupIndex)) {
- return null;
- }
- int adIndexInAdGroup = period.getFirstAdIndexToPlay(adGroupIndex);
- if (!period.isAdAvailable(adGroupIndex, adIndexInAdGroup)) {
- return null;
- }
- long contentDurationUs = period.getDurationUs();
- return getMediaPeriodInfoForAd(
- currentPeriodId.periodUid,
- adGroupIndex,
- adIndexInAdGroup,
- contentDurationUs,
- currentPeriodId.windowSequenceNumber);
}
}
@@ -677,8 +699,6 @@ import com.google.android.exoplayer2.util.Assertions;
long windowSequenceNumber) {
MediaPeriodId id =
new MediaPeriodId(periodUid, adGroupIndex, adIndexInAdGroup, windowSequenceNumber);
- boolean isLastInPeriod = isLastInPeriod(id);
- boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod);
long durationUs =
timeline
.getPeriodByUid(id.periodUid, period)
@@ -691,50 +711,38 @@ import com.google.android.exoplayer2.util.Assertions;
id,
startPositionUs,
contentPositionUs,
+ /* endPositionUs= */ C.TIME_UNSET,
durationUs,
- isLastInPeriod,
- isLastInTimeline);
+ /* isLastInTimelinePeriod= */ false,
+ /* isFinal= */ false);
}
private MediaPeriodInfo getMediaPeriodInfoForContent(
Object periodUid, long startPositionUs, long windowSequenceNumber) {
int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(startPositionUs);
- long endPositionUs =
- nextAdGroupIndex == C.INDEX_UNSET
- ? C.TIME_END_OF_SOURCE
- : period.getAdGroupTimeUs(nextAdGroupIndex);
- MediaPeriodId id = new MediaPeriodId(periodUid, windowSequenceNumber, endPositionUs);
- timeline.getPeriodByUid(id.periodUid, period);
+ MediaPeriodId id = new MediaPeriodId(periodUid, windowSequenceNumber, nextAdGroupIndex);
boolean isLastInPeriod = isLastInPeriod(id);
boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod);
+ long endPositionUs =
+ nextAdGroupIndex != C.INDEX_UNSET
+ ? period.getAdGroupTimeUs(nextAdGroupIndex)
+ : C.TIME_UNSET;
long durationUs =
- endPositionUs == C.TIME_END_OF_SOURCE ? period.getDurationUs() : endPositionUs;
+ endPositionUs == C.TIME_UNSET || endPositionUs == C.TIME_END_OF_SOURCE
+ ? period.durationUs
+ : endPositionUs;
return new MediaPeriodInfo(
- id, startPositionUs, C.TIME_UNSET, durationUs, isLastInPeriod, isLastInTimeline);
+ id,
+ startPositionUs,
+ /* contentPositionUs= */ C.TIME_UNSET,
+ endPositionUs,
+ durationUs,
+ isLastInPeriod,
+ isLastInTimeline);
}
private boolean isLastInPeriod(MediaPeriodId id) {
- int adGroupCount = timeline.getPeriodByUid(id.periodUid, period).getAdGroupCount();
- if (adGroupCount == 0) {
- return true;
- }
-
- int lastAdGroupIndex = adGroupCount - 1;
- boolean isAd = id.isAd();
- if (period.getAdGroupTimeUs(lastAdGroupIndex) != C.TIME_END_OF_SOURCE) {
- // There's no postroll ad.
- return !isAd && id.endPositionUs == C.TIME_END_OF_SOURCE;
- }
-
- int postrollAdCount = period.getAdCountInAdGroup(lastAdGroupIndex);
- if (postrollAdCount == C.LENGTH_UNSET) {
- // We won't know if this is the last ad until we know how many postroll ads there are.
- return false;
- }
-
- boolean isLastAd =
- isAd && id.adGroupIndex == lastAdGroupIndex && id.adIndexInAdGroup == postrollAdCount - 1;
- return isLastAd || (!isAd && period.getFirstAdIndexToPlay(lastAdGroupIndex) == postrollAdCount);
+ return !id.isAd() && id.nextAdGroupIndex == C.INDEX_UNSET;
}
private boolean isLastInTimeline(MediaPeriodId id, boolean isLastMediaPeriodInPeriod) {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java
index 593d3d1fce..e901025a07 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.source.SampleStream;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MediaClock;
@@ -121,6 +122,11 @@ public abstract class NoSampleRenderer implements Renderer, RendererCapabilities
return true;
}
+ @Override
+ public long getReadingPositionUs() {
+ return C.TIME_END_OF_SOURCE;
+ }
+
@Override
public final void setCurrentStreamFinal() {
streamIsFinal = true;
@@ -157,6 +163,12 @@ public abstract class NoSampleRenderer implements Renderer, RendererCapabilities
onDisabled();
}
+ @Override
+ public final void reset() {
+ Assertions.checkState(state == STATE_DISABLED);
+ onReset();
+ }
+
@Override
public boolean isReady() {
return true;
@@ -182,7 +194,7 @@ public abstract class NoSampleRenderer implements Renderer, RendererCapabilities
// PlayerMessage.Target implementation.
@Override
- public void handleMessage(int what, Object object) throws ExoPlaybackException {
+ public void handleMessage(int what, @Nullable Object object) throws ExoPlaybackException {
// Do nothing.
}
@@ -259,6 +271,15 @@ public abstract class NoSampleRenderer implements Renderer, RendererCapabilities
// Do nothing.
}
+ /**
+ * Called when the renderer is reset.
+ *
+ * The default implementation is a no-op.
+ */
+ protected void onReset() {
+ // Do nothing.
+ }
+
// Methods to be called by subclasses.
/**
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 02058c0484..e9b99acd77 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
@@ -15,7 +15,7 @@
*/
package com.google.android.exoplayer2;
-import android.support.annotation.Nullable;
+import androidx.annotation.CheckResult;
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
@@ -29,28 +29,28 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
* Dummy media period id used while the timeline is empty and no period id is specified. This id
* is used when playback infos are created with {@link #createDummy(long, TrackSelectorResult)}.
*/
- public static final MediaPeriodId DUMMY_MEDIA_PERIOD_ID =
+ private static final MediaPeriodId DUMMY_MEDIA_PERIOD_ID =
new MediaPeriodId(/* periodUid= */ new Object());
/** The current {@link Timeline}. */
public final Timeline timeline;
- /** The current manifest. */
- public final @Nullable Object manifest;
/** The {@link MediaPeriodId} of the currently playing media period in the {@link #timeline}. */
public final MediaPeriodId periodId;
/**
* The start position at which playback started in {@link #periodId} relative to the start of the
- * associated period in the {@link #timeline}, in microseconds.
+ * associated period in the {@link #timeline}, in microseconds. Note that this value changes for
+ * each position discontinuity.
*/
public final long startPositionUs;
/**
* If {@link #periodId} refers to an ad, the position of the suspended content relative to the
* start of the associated period in the {@link #timeline}, in microseconds. {@link C#TIME_UNSET}
- * if {@link #periodId} does not refer to an ad.
+ * if {@link #periodId} does not refer to an ad or if the suspended content should be played from
+ * its default position.
*/
public final long contentPositionUs;
/** The current playback state. One of the {@link Player}.STATE_ constants. */
- public final int playbackState;
+ @Player.State public final int playbackState;
/** Whether the player is currently loading. */
public final boolean isLoading;
/** The currently available track groups. */
@@ -89,7 +89,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
long startPositionUs, TrackSelectorResult emptyTrackSelectorResult) {
return new PlaybackInfo(
Timeline.EMPTY,
- /* manifest= */ null,
DUMMY_MEDIA_PERIOD_ID,
startPositionUs,
/* contentPositionUs= */ C.TIME_UNSET,
@@ -103,13 +102,28 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
startPositionUs);
}
+ /**
+ * Create playback info.
+ *
+ * @param timeline See {@link #timeline}.
+ * @param periodId See {@link #periodId}.
+ * @param startPositionUs See {@link #startPositionUs}.
+ * @param contentPositionUs See {@link #contentPositionUs}.
+ * @param playbackState See {@link #playbackState}.
+ * @param isLoading See {@link #isLoading}.
+ * @param trackGroups See {@link #trackGroups}.
+ * @param trackSelectorResult See {@link #trackSelectorResult}.
+ * @param loadingMediaPeriodId See {@link #loadingMediaPeriodId}.
+ * @param bufferedPositionUs See {@link #bufferedPositionUs}.
+ * @param totalBufferedDurationUs See {@link #totalBufferedDurationUs}.
+ * @param positionUs See {@link #positionUs}.
+ */
public PlaybackInfo(
Timeline timeline,
- @Nullable Object manifest,
MediaPeriodId periodId,
long startPositionUs,
long contentPositionUs,
- int playbackState,
+ @Player.State int playbackState,
boolean isLoading,
TrackGroupArray trackGroups,
TrackSelectorResult trackSelectorResult,
@@ -118,7 +132,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
long totalBufferedDurationUs,
long positionUs) {
this.timeline = timeline;
- this.manifest = manifest;
this.periodId = periodId;
this.startPositionUs = startPositionUs;
this.contentPositionUs = contentPositionUs;
@@ -132,28 +145,74 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
this.positionUs = positionUs;
}
- public PlaybackInfo fromNewPosition(
- MediaPeriodId periodId, long startPositionUs, long contentPositionUs) {
+ /**
+ * Returns dummy media period id for the first-to-be-played period of the current timeline.
+ *
+ * @param shuffleModeEnabled Whether shuffle mode is enabled.
+ * @param window A writable {@link Timeline.Window}.
+ * @param period A writable {@link Timeline.Period}.
+ * @return A dummy media period id for the first-to-be-played period of the current timeline.
+ */
+ public MediaPeriodId getDummyFirstMediaPeriodId(
+ boolean shuffleModeEnabled, Timeline.Window window, Timeline.Period period) {
+ if (timeline.isEmpty()) {
+ return DUMMY_MEDIA_PERIOD_ID;
+ }
+ int firstWindowIndex = timeline.getFirstWindowIndex(shuffleModeEnabled);
+ int firstPeriodIndex = timeline.getWindow(firstWindowIndex, window).firstPeriodIndex;
+ int currentPeriodIndex = timeline.getIndexOfPeriod(periodId.periodUid);
+ long windowSequenceNumber = C.INDEX_UNSET;
+ if (currentPeriodIndex != C.INDEX_UNSET) {
+ int currentWindowIndex = timeline.getPeriod(currentPeriodIndex, period).windowIndex;
+ if (firstWindowIndex == currentWindowIndex) {
+ // Keep window sequence number if the new position is still in the same window.
+ windowSequenceNumber = periodId.windowSequenceNumber;
+ }
+ }
+ return new MediaPeriodId(timeline.getUidOfPeriod(firstPeriodIndex), windowSequenceNumber);
+ }
+
+ /**
+ * Copies playback info with new playing position.
+ *
+ * @param periodId New playing media period. See {@link #periodId}.
+ * @param positionUs New position. See {@link #positionUs}.
+ * @param contentPositionUs New content position. See {@link #contentPositionUs}. Value is ignored
+ * if {@code periodId.isAd()} is true.
+ * @param totalBufferedDurationUs New buffered duration. See {@link #totalBufferedDurationUs}.
+ * @return Copied playback info with new playing position.
+ */
+ @CheckResult
+ public PlaybackInfo copyWithNewPosition(
+ MediaPeriodId periodId,
+ long positionUs,
+ long contentPositionUs,
+ long totalBufferedDurationUs) {
return new PlaybackInfo(
timeline,
- manifest,
periodId,
- startPositionUs,
+ positionUs,
periodId.isAd() ? contentPositionUs : C.TIME_UNSET,
playbackState,
isLoading,
trackGroups,
trackSelectorResult,
- periodId,
- startPositionUs,
- /* totalBufferedDurationUs= */ 0,
- startPositionUs);
+ loadingMediaPeriodId,
+ bufferedPositionUs,
+ totalBufferedDurationUs,
+ positionUs);
}
- public PlaybackInfo copyWithTimeline(Timeline timeline, Object manifest) {
+ /**
+ * Copies playback info with the new timeline.
+ *
+ * @param timeline New timeline. See {@link #timeline}.
+ * @return Copied playback info with the new timeline.
+ */
+ @CheckResult
+ public PlaybackInfo copyWithTimeline(Timeline timeline) {
return new PlaybackInfo(
timeline,
- manifest,
periodId,
startPositionUs,
contentPositionUs,
@@ -167,10 +226,16 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
positionUs);
}
+ /**
+ * Copies playback info with new playback state.
+ *
+ * @param playbackState New playback state. See {@link #playbackState}.
+ * @return Copied playback info with new playback state.
+ */
+ @CheckResult
public PlaybackInfo copyWithPlaybackState(int playbackState) {
return new PlaybackInfo(
timeline,
- manifest,
periodId,
startPositionUs,
contentPositionUs,
@@ -184,10 +249,16 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
positionUs);
}
+ /**
+ * Copies playback info with new loading state.
+ *
+ * @param isLoading New loading state. See {@link #isLoading}.
+ * @return Copied playback info with new loading state.
+ */
+ @CheckResult
public PlaybackInfo copyWithIsLoading(boolean isLoading) {
return new PlaybackInfo(
timeline,
- manifest,
periodId,
startPositionUs,
contentPositionUs,
@@ -201,11 +272,18 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
positionUs);
}
+ /**
+ * Copies playback info with new track information.
+ *
+ * @param trackGroups New track groups. See {@link #trackGroups}.
+ * @param trackSelectorResult New track selector result. See {@link #trackSelectorResult}.
+ * @return Copied playback info with new track information.
+ */
+ @CheckResult
public PlaybackInfo copyWithTrackInfo(
TrackGroupArray trackGroups, TrackSelectorResult trackSelectorResult) {
return new PlaybackInfo(
timeline,
- manifest,
periodId,
startPositionUs,
contentPositionUs,
@@ -219,10 +297,16 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
positionUs);
}
+ /**
+ * Copies playback info with new loading media period.
+ *
+ * @param loadingMediaPeriodId New loading media period id. See {@link #loadingMediaPeriodId}.
+ * @return Copied playback info with new loading media period.
+ */
+ @CheckResult
public PlaybackInfo copyWithLoadingMediaPeriodId(MediaPeriodId loadingMediaPeriodId) {
return new PlaybackInfo(
timeline,
- manifest,
periodId,
startPositionUs,
contentPositionUs,
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 6f2db4ff5e..057cb371e5 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
@@ -15,7 +15,7 @@
*/
package com.google.android.exoplayer2;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.util.Assertions;
/**
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 e99d62a417..eed59876f9 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
@@ -16,15 +16,17 @@
package com.google.android.exoplayer2;
import android.os.Looper;
-import android.support.annotation.IntDef;
-import android.support.annotation.Nullable;
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.TextureView;
+import com.google.android.exoplayer2.C.VideoScalingMode;
import com.google.android.exoplayer2.audio.AudioAttributes;
import com.google.android.exoplayer2.audio.AudioListener;
import com.google.android.exoplayer2.audio.AuxEffectInfo;
+import com.google.android.exoplayer2.metadata.MetadataOutput;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.text.TextOutput;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
@@ -32,6 +34,7 @@ import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.VideoFrameMetadataListener;
import com.google.android.exoplayer2.video.VideoListener;
import com.google.android.exoplayer2.video.spherical.CameraMotionListener;
+import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -143,14 +146,14 @@ public interface Player {
interface VideoComponent {
/**
- * Sets the video scaling mode.
+ * Sets the {@link VideoScalingMode}.
*
- * @param videoScalingMode The video scaling mode.
+ * @param videoScalingMode The {@link VideoScalingMode}.
*/
- void setVideoScalingMode(@C.VideoScalingMode int videoScalingMode);
+ void setVideoScalingMode(@VideoScalingMode int videoScalingMode);
- /** Returns the video scaling mode. */
- @C.VideoScalingMode
+ /** Returns the {@link VideoScalingMode}. */
+ @VideoScalingMode
int getVideoScalingMode();
/**
@@ -297,12 +300,53 @@ public interface Player {
void removeTextOutput(TextOutput listener);
}
+ /** The metadata component of a {@link Player}. */
+ interface MetadataComponent {
+
+ /**
+ * Adds a {@link MetadataOutput} to receive metadata.
+ *
+ * @param output The output to register.
+ */
+ void addMetadataOutput(MetadataOutput output);
+
+ /**
+ * Removes a {@link MetadataOutput}.
+ *
+ * @param output The output to remove.
+ */
+ void removeMetadataOutput(MetadataOutput output);
+ }
+
/**
* Listener of changes in player state. All methods have no-op default implementations to allow
* selective overrides.
*/
interface EventListener {
+ /**
+ * Called when the timeline has been refreshed.
+ *
+ *
Note that if the timeline has changed then a position discontinuity may also have
+ * occurred. For example, the current period index may have changed as a result of periods being
+ * added or removed from the timeline. This will not be reported via a separate call to
+ * {@link #onPositionDiscontinuity(int)}.
+ *
+ * @param timeline The latest timeline. Never null, but may be empty.
+ * @param reason The {@link TimelineChangeReason} responsible for this timeline change.
+ */
+ @SuppressWarnings("deprecation")
+ default void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) {
+ Object manifest = null;
+ if (timeline.getWindowCount() == 1) {
+ // Legacy behavior was to report the manifest for single window timelines only.
+ Timeline.Window window = new Timeline.Window();
+ manifest = timeline.getWindow(0, window).manifest;
+ }
+ // Call deprecated version.
+ onTimelineChanged(timeline, manifest, reason);
+ }
+
/**
* Called when the timeline and/or manifest has been refreshed.
*
@@ -314,7 +358,11 @@ public interface Player {
* @param timeline The latest timeline. Never null, but may be empty.
* @param manifest The latest manifest. May be null.
* @param reason The {@link TimelineChangeReason} responsible for this timeline change.
+ * @deprecated Use {@link #onTimelineChanged(Timeline, int)} instead. The manifest can be
+ * accessed by using {@link #getCurrentManifest()} or {@code timeline.getWindow(windowIndex,
+ * window).manifest} for a given window index.
*/
+ @Deprecated
default void onTimelineChanged(
Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {}
@@ -340,9 +388,9 @@ public interface Player {
* #getPlaybackState()} changes.
*
* @param playWhenReady Whether playback will proceed when ready.
- * @param playbackState One of the {@code STATE} constants.
+ * @param playbackState The new {@link State playback state}.
*/
- default void onPlayerStateChanged(boolean playWhenReady, int playbackState) {}
+ default void onPlayerStateChanged(boolean playWhenReady, @State int playbackState) {}
/**
* Called when the value of {@link #getRepeatMode()} changes.
@@ -375,8 +423,7 @@ public interface Player {
* when the source introduces a discontinuity internally).
*
*
When a position discontinuity occurs as a result of a change to the timeline this method
- * is not called. {@link #onTimelineChanged(Timeline, Object, int)} is called in this
- * case.
+ * is not called. {@link #onTimelineChanged(Timeline, int)} is called in this case.
*
* @param reason The {@link DiscontinuityReason} responsible for the discontinuity.
*/
@@ -407,6 +454,19 @@ public interface Player {
@Deprecated
abstract class DefaultEventListener implements EventListener {
+ @Override
+ @SuppressWarnings("deprecation")
+ public void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) {
+ Object manifest = null;
+ if (timeline.getWindowCount() == 1) {
+ // Legacy behavior was to report the manifest for single window timelines only.
+ Timeline.Window window = new Timeline.Window();
+ manifest = timeline.getWindow(0, window).manifest;
+ }
+ // Call deprecated version.
+ onTimelineChanged(timeline, manifest, reason);
+ }
+
@Override
@SuppressWarnings("deprecation")
public void onTimelineChanged(
@@ -415,13 +475,21 @@ public interface Player {
onTimelineChanged(timeline, manifest);
}
- /** @deprecated Use {@link EventListener#onTimelineChanged(Timeline, Object, int)} instead. */
+ /** @deprecated Use {@link EventListener#onTimelineChanged(Timeline, int)} instead. */
@Deprecated
public void onTimelineChanged(Timeline timeline, @Nullable Object manifest) {
// Do nothing.
}
}
+ /**
+ * Playback state. One of {@link #STATE_IDLE}, {@link #STATE_BUFFERING}, {@link #STATE_READY} or
+ * {@link #STATE_ENDED}.
+ */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({STATE_IDLE, STATE_BUFFERING, STATE_READY, STATE_ENDED})
+ @interface State {}
/**
* The player does not have any media to play.
*/
@@ -445,6 +513,7 @@ public interface Player {
* Repeat modes for playback. One of {@link #REPEAT_MODE_OFF}, {@link #REPEAT_MODE_ONE} or {@link
* #REPEAT_MODE_ALL}.
*/
+ @Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({REPEAT_MODE_OFF, REPEAT_MODE_ONE, REPEAT_MODE_ALL})
@interface RepeatMode {}
@@ -466,6 +535,7 @@ public interface Player {
* {@link #DISCONTINUITY_REASON_SEEK}, {@link #DISCONTINUITY_REASON_SEEK_ADJUSTMENT}, {@link
* #DISCONTINUITY_REASON_AD_INSERTION} or {@link #DISCONTINUITY_REASON_INTERNAL}.
*/
+ @Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({
DISCONTINUITY_REASON_PERIOD_TRANSITION,
@@ -493,9 +563,10 @@ public interface Player {
int DISCONTINUITY_REASON_INTERNAL = 4;
/**
- * Reasons for timeline and/or manifest changes. One of {@link #TIMELINE_CHANGE_REASON_PREPARED},
- * {@link #TIMELINE_CHANGE_REASON_RESET} or {@link #TIMELINE_CHANGE_REASON_DYNAMIC}.
+ * Reasons for timeline changes. One of {@link #TIMELINE_CHANGE_REASON_PREPARED}, {@link
+ * #TIMELINE_CHANGE_REASON_RESET} or {@link #TIMELINE_CHANGE_REASON_DYNAMIC}.
*/
+ @Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({
TIMELINE_CHANGE_REASON_PREPARED,
@@ -528,6 +599,18 @@ public interface Player {
@Nullable
TextComponent getTextComponent();
+ /**
+ * Returns the component of this player for metadata output, or null if metadata is not supported.
+ */
+ @Nullable
+ MetadataComponent getMetadataComponent();
+
+ /**
+ * Returns the {@link Looper} associated with the application thread that's used to access the
+ * player and on which player events are received.
+ */
+ Looper getApplicationLooper();
+
/**
* Register a listener to receive events from the player. The listener's methods will be called on
* the thread that was used to construct the player. However, if the thread used to construct the
@@ -545,10 +628,11 @@ public interface Player {
void removeListener(EventListener listener);
/**
- * Returns the current state of the player.
+ * Returns the current {@link State playback state} of the player.
*
- * @return One of the {@code STATE} constants defined in this interface.
+ * @return The current {@link State playback state}.
*/
+ @State
int getPlaybackState();
/**
@@ -648,16 +732,39 @@ public interface Player {
*/
void seekTo(int windowIndex, long positionMs);
+ /**
+ * Returns whether a previous window exists, which may depend on the current repeat mode and
+ * whether shuffle mode is enabled.
+ */
+ boolean hasPrevious();
+
+ /**
+ * Seeks to the default position of the previous window in the timeline, which may depend on the
+ * current repeat mode and whether shuffle mode is enabled. Does nothing if {@link #hasPrevious()}
+ * is {@code false}.
+ */
+ void previous();
+
+ /**
+ * Returns whether a next window exists, which may depend on the current repeat mode and whether
+ * shuffle mode is enabled.
+ */
+ boolean hasNext();
+
+ /**
+ * Seeks to the default position of the next window in the timeline, which may depend on the
+ * current repeat mode and whether shuffle mode is enabled. Does nothing if {@link #hasNext()} is
+ * {@code false}.
+ */
+ void next();
+
/**
* Attempts to set the playback parameters. Passing {@code null} sets the parameters to the
* default, {@link PlaybackParameters#DEFAULT}, which means there is no speed or pitch adjustment.
- *
- * Playback parameters changes may cause the player to buffer.
- * {@link EventListener#onPlaybackParametersChanged(PlaybackParameters)} will be called whenever
- * the currently active playback parameters change. When that listener is called, the parameters
- * passed to it may not match {@code playbackParameters}. For example, the chosen speed or pitch
- * may be out of range, in which case they are constrained to a set of permitted values. If it is
- * not possible to change the playback parameters, the listener will not be invoked.
+ *
+ *
Playback parameters changes may cause the player to buffer. {@link
+ * EventListener#onPlaybackParametersChanged(PlaybackParameters)} will be called whenever the
+ * currently active playback parameters change.
*
* @param playbackParameters The playback parameters, or {@code null} to use the defaults.
*/
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java b/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java
index d2b8d72b1e..49309181a0 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java
@@ -16,7 +16,7 @@
package com.google.android.exoplayer2;
import android.os.Handler;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.util.Assertions;
/**
@@ -36,7 +36,7 @@ public final class PlayerMessage {
* @throws ExoPlaybackException If an error occurred whilst handling the message. Should only be
* thrown by targets that handle messages on the playback thread.
*/
- void handleMessage(int messageType, Object payload) throws ExoPlaybackException;
+ void handleMessage(int messageType, @Nullable Object payload) throws ExoPlaybackException;
}
/** A sender for messages. */
@@ -55,7 +55,7 @@ public final class PlayerMessage {
private final Timeline timeline;
private int type;
- private Object payload;
+ @Nullable private Object payload;
private Handler handler;
private int windowIndex;
private long positionMs;
@@ -134,6 +134,7 @@ public final class PlayerMessage {
}
/** Returns the message payload forwarded to {@link Target#handleMessage(int, Object)}. */
+ @Nullable
public Object getPayload() {
return payload;
}
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 d1e1541cdc..9f52e8d9de 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,10 +15,11 @@
*/
package com.google.android.exoplayer2;
-import android.support.annotation.IntDef;
+import androidx.annotation.IntDef;
import com.google.android.exoplayer2.source.SampleStream;
import com.google.android.exoplayer2.util.MediaClock;
import java.io.IOException;
+import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -38,16 +39,21 @@ public interface Renderer extends PlayerMessage.Target {
* The renderer states. One of {@link #STATE_DISABLED}, {@link #STATE_ENABLED} or {@link
* #STATE_STARTED}.
*/
+ @Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({STATE_DISABLED, STATE_ENABLED, STATE_STARTED})
@interface State {}
/**
- * The renderer is disabled.
+ * The renderer is disabled. A renderer in this state may hold resources that it requires for
+ * rendering (e.g. media decoders), for use if it's subsequently enabled. {@link #reset()} can be
+ * called to force the renderer to release these resources.
*/
int STATE_DISABLED = 0;
/**
- * The renderer is enabled but not started. A renderer in this state is not actively rendering
- * media, but will typically hold resources that it requires for rendering (e.g. media decoders).
+ * The renderer is enabled but not started. A renderer in this state may render media at the
+ * current position (e.g. an initial video frame), but the position will not advance. A renderer
+ * in this state will typically hold resources that it requires for rendering (e.g. media
+ * decoders).
*/
int STATE_ENABLED = 1;
/**
@@ -154,6 +160,16 @@ public interface Renderer extends PlayerMessage.Target {
*/
boolean hasReadStreamToEnd();
+ /**
+ * Returns the playback position up to which the renderer has read samples from the current {@link
+ * SampleStream}, in microseconds, or {@link C#TIME_END_OF_SOURCE} if the renderer has read the
+ * current {@link SampleStream} to the end.
+ *
+ *
This method may be called when the renderer is in the following states: {@link
+ * #STATE_ENABLED}, {@link #STATE_STARTED}.
+ */
+ long getReadingPositionUs();
+
/**
* Signals to the renderer that the current {@link SampleStream} will be the final one supplied
* before it is next disabled or reset.
@@ -277,4 +293,12 @@ public interface Renderer extends PlayerMessage.Target {
*/
void disable();
+ /**
+ * Forces the renderer to give up any resources (e.g. media decoders) that it may be holding. If
+ * the renderer is not holding any resources, the call is a no-op.
+ *
+ *
This method may be called when the renderer is in the following states: {@link
+ * #STATE_DISABLED}.
+ */
+ void reset();
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/RendererConfiguration.java b/library/core/src/main/java/com/google/android/exoplayer2/RendererConfiguration.java
index 684072efc6..bc8c6ff633 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/RendererConfiguration.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/RendererConfiguration.java
@@ -15,7 +15,7 @@
*/
package com.google.android.exoplayer2;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
/**
* The configuration of a {@link Renderer}.
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 e221898471..6f0d125bcf 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,7 @@
package com.google.android.exoplayer2;
import android.os.Handler;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SeekParameters.java b/library/core/src/main/java/com/google/android/exoplayer2/SeekParameters.java
index ca0433f96d..7a0ad67a28 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/SeekParameters.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/SeekParameters.java
@@ -15,7 +15,7 @@
*/
package com.google.android.exoplayer2;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.util.Assertions;
/**
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 c29055ddc7..8913fbdaba 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
@@ -23,8 +23,7 @@ import android.media.MediaCodec;
import android.media.PlaybackParams;
import android.os.Handler;
import android.os.Looper;
-import android.support.annotation.Nullable;
-import android.util.Log;
+import androidx.annotation.Nullable;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
@@ -49,7 +48,10 @@ import com.google.android.exoplayer2.text.TextOutput;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.trackselection.TrackSelector;
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.util.Log;
+import com.google.android.exoplayer2.util.PriorityTaskManager;
import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.VideoFrameMetadataListener;
import com.google.android.exoplayer2.video.VideoRendererEventListener;
@@ -63,9 +65,12 @@ import java.util.concurrent.CopyOnWriteArraySet;
* An {@link ExoPlayer} implementation that uses default {@link Renderer} components. Instances can
* be obtained from {@link ExoPlayerFactory}.
*/
-@TargetApi(16)
-public class SimpleExoPlayer
- implements ExoPlayer, Player.AudioComponent, Player.VideoComponent, Player.TextComponent {
+public class SimpleExoPlayer extends BasePlayer
+ implements ExoPlayer,
+ Player.AudioComponent,
+ Player.VideoComponent,
+ Player.TextComponent,
+ Player.MetadataComponent {
/** @deprecated Use {@link com.google.android.exoplayer2.video.VideoListener}. */
@Deprecated
@@ -90,25 +95,28 @@ public class SimpleExoPlayer
private final AudioFocusManager audioFocusManager;
- private Format videoFormat;
- private Format audioFormat;
+ @Nullable private Format videoFormat;
+ @Nullable private Format audioFormat;
- private Surface surface;
+ @Nullable private Surface surface;
private boolean ownsSurface;
private @C.VideoScalingMode int videoScalingMode;
- private SurfaceHolder surfaceHolder;
- private TextureView textureView;
+ @Nullable private SurfaceHolder surfaceHolder;
+ @Nullable private TextureView textureView;
private int surfaceWidth;
private int surfaceHeight;
- private DecoderCounters videoDecoderCounters;
- private DecoderCounters audioDecoderCounters;
+ @Nullable private DecoderCounters videoDecoderCounters;
+ @Nullable private DecoderCounters audioDecoderCounters;
private int audioSessionId;
private AudioAttributes audioAttributes;
private float audioVolume;
- private MediaSource mediaSource;
+ @Nullable private MediaSource mediaSource;
private List currentCues;
- private VideoFrameMetadataListener videoFrameMetadataListener;
- private CameraMotionListener cameraMotionListener;
+ @Nullable private VideoFrameMetadataListener videoFrameMetadataListener;
+ @Nullable private CameraMotionListener cameraMotionListener;
+ private boolean hasNotifiedFullWrongThreadWarning;
+ @Nullable private PriorityTaskManager priorityTaskManager;
+ private boolean isPriorityTaskManagerRegistered;
/**
* @param context A {@link Context}.
@@ -136,7 +144,7 @@ public class SimpleExoPlayer
loadControl,
drmSessionManager,
bandwidthMeter,
- new AnalyticsCollector.Factory(),
+ new AnalyticsCollector(Clock.DEFAULT),
looper);
}
@@ -148,8 +156,8 @@ public class SimpleExoPlayer
* @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
* will not be used for DRM protected playbacks.
* @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance.
- * @param analyticsCollectorFactory A factory for creating the {@link AnalyticsCollector} that
- * will collect and forward all player events.
+ * @param analyticsCollector The {@link AnalyticsCollector} that will collect and forward all
+ * player events.
* @param looper The {@link Looper} which must be used for all calls to the player and which is
* used to call listeners on.
*/
@@ -160,7 +168,7 @@ public class SimpleExoPlayer
LoadControl loadControl,
@Nullable DrmSessionManager drmSessionManager,
BandwidthMeter bandwidthMeter,
- AnalyticsCollector.Factory analyticsCollectorFactory,
+ AnalyticsCollector analyticsCollector,
Looper looper) {
this(
context,
@@ -169,7 +177,7 @@ public class SimpleExoPlayer
loadControl,
drmSessionManager,
bandwidthMeter,
- analyticsCollectorFactory,
+ analyticsCollector,
Clock.DEFAULT,
looper);
}
@@ -182,8 +190,8 @@ public class SimpleExoPlayer
* @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
* will not be used for DRM protected playbacks.
* @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance.
- * @param analyticsCollectorFactory A factory for creating the {@link AnalyticsCollector} that
- * will collect and forward all player events.
+ * @param analyticsCollector 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.
* @param looper The {@link Looper} which must be used for all calls to the player and which is
@@ -196,10 +204,11 @@ public class SimpleExoPlayer
LoadControl loadControl,
@Nullable DrmSessionManager