diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MetadataRetriever.java b/library/core/src/main/java/com/google/android/exoplayer2/MetadataRetriever.java
new file mode 100644
index 0000000000..c233845e0c
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/MetadataRetriever.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright 2020 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 static com.google.android.exoplayer2.util.Assertions.checkNotNull;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import com.google.android.exoplayer2.source.DefaultMediaSourceFactory;
+import com.google.android.exoplayer2.source.MediaPeriod;
+import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.MediaSourceFactory;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.upstream.Allocator;
+import com.google.android.exoplayer2.upstream.DefaultAllocator;
+import com.google.android.exoplayer2.util.Util;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.SettableFuture;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+
+// TODO(internal b/161127201): discard samples written to the sample queue.
+/** Retrieves the static metadata of {@link MediaItem MediaItems}. */
+public final class MetadataRetriever {
+
+ private MetadataRetriever() {}
+
+ /**
+ * Retrieves the {@link TrackGroupArray} corresponding to a {@link MediaItem}.
+ *
+ *
This is equivalent to using {@code
+ * retrieveMetadata(DefaultMediaSourceFactory.newInstance(context), mediaItem)}.
+ *
+ * @param context The {@link Context}.
+ * @param mediaItem The {@link MediaItem} whose metadata should be retrieved.
+ * @return A {@link ListenableFuture} of the result.
+ */
+ public static ListenableFuture retrieveMetadata(
+ Context context, MediaItem mediaItem) {
+ return retrieveMetadata(DefaultMediaSourceFactory.newInstance(context), mediaItem);
+ }
+
+ /**
+ * Retrieves the {@link TrackGroupArray} corresponding to a {@link MediaItem}.
+ *
+ * This method is thread-safe.
+ *
+ * @param mediaSourceFactory mediaSourceFactory The {@link MediaSourceFactory} to use to read the
+ * data.
+ * @param mediaItem The {@link MediaItem} whose metadata should be retrieved.
+ * @return A {@link ListenableFuture} of the result.
+ */
+ public static ListenableFuture retrieveMetadata(
+ MediaSourceFactory mediaSourceFactory, MediaItem mediaItem) {
+ // Recreate thread and handler every time this method is called so that it can be used
+ // concurrently.
+ return new MetadataRetrieverInternal(mediaSourceFactory).retrieveMetadata(mediaItem);
+ }
+
+ private static final class MetadataRetrieverInternal {
+
+ private static final int MESSAGE_PREPARE_SOURCE = 0;
+ private static final int MESSAGE_CHECK_FOR_FAILURE = 1;
+ private static final int MESSAGE_CONTINUE_LOADING = 2;
+ private static final int MESSAGE_RELEASE = 3;
+
+ private final MediaSourceFactory mediaSourceFactory;
+ private final HandlerThread mediaSourceThread;
+ private final Handler mediaSourceHandler;
+ private final SettableFuture trackGroupsFuture;
+
+ public MetadataRetrieverInternal(MediaSourceFactory mediaSourceFactory) {
+ this.mediaSourceFactory = mediaSourceFactory;
+ mediaSourceThread = new HandlerThread("ExoPlayer:MetadataRetriever");
+ mediaSourceThread.start();
+ mediaSourceHandler =
+ Util.createHandler(mediaSourceThread.getLooper(), new MediaSourceHandlerCallback());
+ trackGroupsFuture = SettableFuture.create();
+ }
+
+ public ListenableFuture retrieveMetadata(MediaItem mediaItem) {
+ mediaSourceHandler.obtainMessage(MESSAGE_PREPARE_SOURCE, mediaItem).sendToTarget();
+ return trackGroupsFuture;
+ }
+
+ private final class MediaSourceHandlerCallback implements Handler.Callback {
+
+ private static final int ERROR_POLL_INTERVAL_MS = 100;
+
+ private final MediaSourceCaller mediaSourceCaller;
+
+ private @MonotonicNonNull MediaSource mediaSource;
+ private @MonotonicNonNull MediaPeriod mediaPeriod;
+
+ public MediaSourceHandlerCallback() {
+ mediaSourceCaller = new MediaSourceCaller();
+ }
+
+ @Override
+ public boolean handleMessage(Message msg) {
+ switch (msg.what) {
+ case MESSAGE_PREPARE_SOURCE:
+ MediaItem mediaItem = (MediaItem) msg.obj;
+ mediaSource = mediaSourceFactory.createMediaSource(mediaItem);
+ mediaSource.prepareSource(mediaSourceCaller, /* mediaTransferListener= */ null);
+ mediaSourceHandler.sendEmptyMessage(MESSAGE_CHECK_FOR_FAILURE);
+ return true;
+ case MESSAGE_CHECK_FOR_FAILURE:
+ try {
+ if (mediaPeriod == null) {
+ checkNotNull(mediaSource).maybeThrowSourceInfoRefreshError();
+ } else {
+ mediaPeriod.maybeThrowPrepareError();
+ }
+ mediaSourceHandler.sendEmptyMessageDelayed(
+ MESSAGE_CHECK_FOR_FAILURE, /* delayMillis= */ ERROR_POLL_INTERVAL_MS);
+ } catch (Exception e) {
+ trackGroupsFuture.setException(e);
+ mediaSourceHandler.obtainMessage(MESSAGE_RELEASE).sendToTarget();
+ }
+ return true;
+ case MESSAGE_CONTINUE_LOADING:
+ checkNotNull(mediaPeriod).continueLoading(/* positionUs= */ 0);
+ return true;
+ case MESSAGE_RELEASE:
+ if (mediaPeriod != null) {
+ checkNotNull(mediaSource).releasePeriod(mediaPeriod);
+ }
+ checkNotNull(mediaSource).releaseSource(mediaSourceCaller);
+ mediaSourceHandler.removeCallbacksAndMessages(/* token= */ null);
+ mediaSourceThread.quit();
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ private final class MediaSourceCaller implements MediaSource.MediaSourceCaller {
+
+ private final MediaPeriodCallback mediaPeriodCallback;
+ private final Allocator allocator;
+
+ private boolean mediaPeriodCreated;
+
+ public MediaSourceCaller() {
+ mediaPeriodCallback = new MediaPeriodCallback();
+ allocator =
+ new DefaultAllocator(
+ /* trimOnReset= */ true,
+ /* individualAllocationSize= */ C.DEFAULT_BUFFER_SEGMENT_SIZE);
+ }
+
+ @Override
+ public void onSourceInfoRefreshed(MediaSource source, Timeline timeline) {
+ if (mediaPeriodCreated) {
+ // Ignore dynamic updates.
+ return;
+ }
+ mediaPeriodCreated = true;
+ mediaPeriod =
+ source.createPeriod(
+ new MediaSource.MediaPeriodId(timeline.getUidOfPeriod(/* periodIndex= */ 0)),
+ allocator,
+ /* startPositionUs= */ 0);
+ mediaPeriod.prepare(mediaPeriodCallback, /* positionUs= */ 0);
+ }
+
+ private final class MediaPeriodCallback implements MediaPeriod.Callback {
+
+ @Override
+ public void onPrepared(MediaPeriod mediaPeriod) {
+ trackGroupsFuture.set(mediaPeriod.getTrackGroups());
+ mediaSourceHandler.obtainMessage(MESSAGE_RELEASE).sendToTarget();
+ }
+
+ @Override
+ public void onContinueLoadingRequested(MediaPeriod mediaPeriod) {
+ mediaSourceHandler.obtainMessage(MESSAGE_CONTINUE_LOADING).sendToTarget();
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/MetadataRetrieverTest.java b/library/core/src/test/java/com/google/android/exoplayer2/MetadataRetrieverTest.java
new file mode 100644
index 0000000000..09b546e89e
--- /dev/null
+++ b/library/core/src/test/java/com/google/android/exoplayer2/MetadataRetrieverTest.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2020 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 static com.google.android.exoplayer2.MetadataRetriever.retrieveMetadata;
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.SystemClock;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.common.util.concurrent.ListenableFuture;
+import java.util.concurrent.ExecutionException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.LooperMode;
+
+/** Tests for {@link MetadataRetriever}. */
+@RunWith(AndroidJUnit4.class)
+@LooperMode(LooperMode.Mode.PAUSED)
+public class MetadataRetrieverTest {
+
+ @Test
+ public void retrieveMetadata_singleMediaItem() throws Exception {
+ Context context = ApplicationProvider.getApplicationContext();
+ MediaItem mediaItem =
+ MediaItem.fromUri(Uri.parse("asset://android_asset/media/mp4/sample.mp4"));
+
+ ListenableFuture trackGroupsFuture = retrieveMetadata(context, mediaItem);
+ TrackGroupArray trackGroups = waitAndGetTrackGroups(trackGroupsFuture);
+
+ assertThat(trackGroups.length).isEqualTo(2);
+ // Video group.
+ assertThat(trackGroups.get(0).length).isEqualTo(1);
+ assertThat(trackGroups.get(0).getFormat(0).sampleMimeType).isEqualTo(MimeTypes.VIDEO_H264);
+ // Audio group.
+ assertThat(trackGroups.get(1).length).isEqualTo(1);
+ assertThat(trackGroups.get(1).getFormat(0).sampleMimeType).isEqualTo(MimeTypes.AUDIO_AAC);
+ }
+
+ @Test
+ public void retrieveMetadata_multipleMediaItems() throws Exception {
+ Context context = ApplicationProvider.getApplicationContext();
+ MediaItem mediaItem1 =
+ MediaItem.fromUri(Uri.parse("asset://android_asset/media/mp4/sample.mp4"));
+ MediaItem mediaItem2 =
+ MediaItem.fromUri(Uri.parse("asset://android_asset/media/mp3/bear-id3.mp3"));
+
+ ListenableFuture trackGroupsFuture1 = retrieveMetadata(context, mediaItem1);
+ ListenableFuture trackGroupsFuture2 = retrieveMetadata(context, mediaItem2);
+ TrackGroupArray trackGroups1 = waitAndGetTrackGroups(trackGroupsFuture1);
+ TrackGroupArray trackGroups2 = waitAndGetTrackGroups(trackGroupsFuture2);
+
+ // First track group.
+ assertThat(trackGroups1.length).isEqualTo(2);
+ // First track group - Video group.
+ assertThat(trackGroups1.get(0).length).isEqualTo(1);
+ assertThat(trackGroups1.get(0).getFormat(0).sampleMimeType).isEqualTo(MimeTypes.VIDEO_H264);
+ // First track group - Audio group.
+ assertThat(trackGroups1.get(1).length).isEqualTo(1);
+ assertThat(trackGroups1.get(1).getFormat(0).sampleMimeType).isEqualTo(MimeTypes.AUDIO_AAC);
+
+ // Second track group.
+ assertThat(trackGroups2.length).isEqualTo(1);
+ // Second track group - Audio group.
+ assertThat(trackGroups2.get(0).length).isEqualTo(1);
+ assertThat(trackGroups2.get(0).getFormat(0).sampleMimeType).isEqualTo(MimeTypes.AUDIO_MPEG);
+ }
+
+ @Test
+ public void retrieveMetadata_throwsErrorIfCannotLoad() {
+ Context context = ApplicationProvider.getApplicationContext();
+ MediaItem mediaItem =
+ MediaItem.fromUri(Uri.parse("asset://android_asset/media/does_not_exist"));
+
+ ListenableFuture trackGroupsFuture = retrieveMetadata(context, mediaItem);
+
+ assertThrows(ExecutionException.class, () -> waitAndGetTrackGroups(trackGroupsFuture));
+ }
+
+ private static TrackGroupArray waitAndGetTrackGroups(
+ ListenableFuture trackGroupsFuture)
+ throws InterruptedException, ExecutionException {
+ while (!trackGroupsFuture.isDone()) {
+ // Simulate advancing SystemClock so that delayed messages sent to handlers are received.
+ SystemClock.setCurrentTimeMillis(SystemClock.uptimeMillis() + 100);
+ Thread.sleep(/* millis= */ 100);
+ }
+ return trackGroupsFuture.get();
+ }
+}
diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java
index 87d82db2f8..ae523b4387 100644
--- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java
+++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java
@@ -1097,7 +1097,7 @@ public class StyledPlayerControlView extends FrameLayout {
return;
}
if (playPauseButton != null) {
- if (player != null && player.getPlayWhenReady()) {
+ if (shouldShowPauseButton()) {
((ImageView) playPauseButton)
.setImageDrawable(resources.getDrawable(R.drawable.exo_styled_controls_pause));
playPauseButton.setContentDescription(
@@ -1665,6 +1665,13 @@ public class StyledPlayerControlView extends FrameLayout {
return true;
}
+ private boolean shouldShowPauseButton() {
+ return player != null
+ && player.getPlaybackState() != Player.STATE_ENDED
+ && player.getPlaybackState() != Player.STATE_IDLE
+ && player.getPlayWhenReady();
+ }
+
@SuppressLint("InlinedApi")
private static boolean isHandledMediaKey(int keyCode) {
return keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD