Fix bug to show 'play' button at the end of stream
PiperOrigin-RevId: 327158791
This commit is contained in:
parent
103bb98dba
commit
b853978a91
@ -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}.
|
||||||
|
*
|
||||||
|
* <p>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<TrackGroupArray> retrieveMetadata(
|
||||||
|
Context context, MediaItem mediaItem) {
|
||||||
|
return retrieveMetadata(DefaultMediaSourceFactory.newInstance(context), mediaItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the {@link TrackGroupArray} corresponding to a {@link MediaItem}.
|
||||||
|
*
|
||||||
|
* <p>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<TrackGroupArray> 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<TrackGroupArray> 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<TrackGroupArray> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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<TrackGroupArray> 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<TrackGroupArray> trackGroupsFuture1 = retrieveMetadata(context, mediaItem1);
|
||||||
|
ListenableFuture<TrackGroupArray> 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<TrackGroupArray> trackGroupsFuture = retrieveMetadata(context, mediaItem);
|
||||||
|
|
||||||
|
assertThrows(ExecutionException.class, () -> waitAndGetTrackGroups(trackGroupsFuture));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TrackGroupArray waitAndGetTrackGroups(
|
||||||
|
ListenableFuture<TrackGroupArray> 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();
|
||||||
|
}
|
||||||
|
}
|
@ -1097,7 +1097,7 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (playPauseButton != null) {
|
if (playPauseButton != null) {
|
||||||
if (player != null && player.getPlayWhenReady()) {
|
if (shouldShowPauseButton()) {
|
||||||
((ImageView) playPauseButton)
|
((ImageView) playPauseButton)
|
||||||
.setImageDrawable(resources.getDrawable(R.drawable.exo_styled_controls_pause));
|
.setImageDrawable(resources.getDrawable(R.drawable.exo_styled_controls_pause));
|
||||||
playPauseButton.setContentDescription(
|
playPauseButton.setContentDescription(
|
||||||
@ -1665,6 +1665,13 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean shouldShowPauseButton() {
|
||||||
|
return player != null
|
||||||
|
&& player.getPlaybackState() != Player.STATE_ENDED
|
||||||
|
&& player.getPlaybackState() != Player.STATE_IDLE
|
||||||
|
&& player.getPlayWhenReady();
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
@SuppressLint("InlinedApi")
|
||||||
private static boolean isHandledMediaKey(int keyCode) {
|
private static boolean isHandledMediaKey(int keyCode) {
|
||||||
return keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD
|
return keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD
|
||||||
|
Loading…
x
Reference in New Issue
Block a user