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;
|
||||
}
|
||||
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
|
||||
|
Loading…
x
Reference in New Issue
Block a user