Fix bug to show 'play' button at the end of stream

PiperOrigin-RevId: 327158791
This commit is contained in:
insun 2020-08-18 05:11:03 +01:00 committed by kim-vde
parent 103bb98dba
commit b853978a91
3 changed files with 316 additions and 1 deletions

View File

@ -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();
}
}
}
}
}
}

View File

@ -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();
}
}

View File

@ -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