Add error and buffering views to PlayerView

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=195203362
This commit is contained in:
olly 2018-05-02 22:32:16 -07:00 committed by Oliver Woodman
parent 7799e8fd5e
commit b23eabd939
10 changed files with 196 additions and 62 deletions

View File

@ -12,9 +12,12 @@
`SimpleExoPlayer` to receive detailed meta data for each ExoPlayer event.
* Added `getPlaybackError` to `Player` interface.
* UI components:
* Add support for displaying error messages and a buffering spinner in
`PlayerView`.
* Add support for listening to `AspectRatioFrameLayout`'s aspect ratio update
([#3736](https://github.com/google/ExoPlayer/issues/3736)).
* Add PlayerNotificationManager.
* Add `PlayerNotificationManager` for displaying notifications reflecting the
player state.
* Downloading: Add `DownloadService`, `DownloadManager` and
related classes ([#2643](https://github.com/google/ExoPlayer/issues/2643)).
* MediaSources:

View File

@ -81,6 +81,7 @@ import com.google.android.exoplayer2.ui.TrackSelectionView;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.util.ErrorMessageProvider;
import com.google.android.exoplayer2.util.EventLogger;
import com.google.android.exoplayer2.util.ParcelableArray;
import com.google.android.exoplayer2.util.Util;
@ -143,7 +144,6 @@ public class PlayerActivity extends Activity
private DefaultTrackSelector trackSelector;
private DefaultTrackSelector.Parameters trackSelectorParameters;
private DebugTextViewHelper debugViewHelper;
private boolean inErrorState;
private TrackGroupArray lastSeenTrackGroupArray;
private boolean startAutoPlay;
@ -174,6 +174,7 @@ public class PlayerActivity extends Activity
playerView = findViewById(R.id.player_view);
playerView.setControllerVisibilityListener(this);
playerView.setErrorMessageProvider(new PlayerErrorMessageProvider());
playerView.requestFocus();
if (savedInstanceState != null) {
@ -235,17 +236,17 @@ public class PlayerActivity extends Activity
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
if (grantResults.length > 0) {
if (grantResults.length == 0) {
// Empty results are triggered if a permission is requested while another request was already
// pending and can be safely ignored in this case.
return;
}
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
initializePlayer();
} else {
showToast(R.string.storage_permission_denied);
finish();
}
} else {
// Empty results are triggered if a permission is requested while another request was already
// pending and can be safely ignored in this case.
}
}
@Override
@ -440,7 +441,6 @@ public class PlayerActivity extends Activity
player.seekTo(startWindow, startPosition);
}
player.prepare(mediaSource, !haveStartPosition, false);
inErrorState = false;
updateButtonVisibilities();
}
@ -486,8 +486,10 @@ public class PlayerActivity extends Activity
private DefaultDrmSessionManager<FrameworkMediaCrypto> buildDrmSessionManagerV18(
UUID uuid, String licenseUrl, String[] keyRequestPropertiesArray, boolean multiSession)
throws UnsupportedDrmException {
HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl,
buildHttpDataSourceFactory(false));
HttpDataSource.Factory licenseDataSourceFactory =
((DemoApplication) getApplication()).buildHttpDataSourceFactory(/* listener= */ null);
HttpMediaDrmCallback drmCallback =
new HttpMediaDrmCallback(licenseUrl, licenseDataSourceFactory);
if (keyRequestPropertiesArray != null) {
for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) {
drmCallback.setKeyRequestProperty(keyRequestPropertiesArray[i],
@ -543,18 +545,6 @@ public class PlayerActivity extends Activity
.buildDataSourceFactory(useBandwidthMeter ? BANDWIDTH_METER : null);
}
/**
* Returns a new HttpDataSource factory.
*
* @param useBandwidthMeter Whether to set {@link #BANDWIDTH_METER} as a listener to the new
* DataSource factory.
* @return A new HttpDataSource factory.
*/
private HttpDataSource.Factory buildHttpDataSourceFactory(boolean useBandwidthMeter) {
return ((DemoApplication) getApplication())
.buildHttpDataSourceFactory(useBandwidthMeter ? BANDWIDTH_METER : null);
}
/** Returns an ads media source, reusing the ads loader if one exists. */
private @Nullable MediaSource createAdsMediaSource(MediaSource mediaSource, Uri adTagUri) {
// Load the extension source using reflection so the demo app doesn't have to depend on it.
@ -623,7 +613,7 @@ public class PlayerActivity extends Activity
return;
}
for (int i = 0; i < mappedTrackInfo.length; i++) {
for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) {
TrackGroupArray trackGroups = mappedTrackInfo.getTrackGroups(i);
if (trackGroups.length != 0) {
Button button = new Button(this);
@ -687,43 +677,15 @@ public class PlayerActivity extends Activity
@Override
public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
if (inErrorState) {
// This will only occur if the user has performed a seek whilst in the error state. Update
// the resume position so that if the user then retries, playback will resume from the
// position to which they seeked.
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();
}
}
@Override
public void onPlayerError(ExoPlaybackException e) {
String errorString = null;
if (e.type == ExoPlaybackException.TYPE_RENDERER) {
Exception cause = e.getRendererException();
if (cause instanceof DecoderInitializationException) {
// Special case for decoder initialization failures.
DecoderInitializationException decoderInitializationException =
(DecoderInitializationException) cause;
if (decoderInitializationException.decoderName == null) {
if (decoderInitializationException.getCause() instanceof DecoderQueryException) {
errorString = getString(R.string.error_querying_decoders);
} else if (decoderInitializationException.secureDecoderRequired) {
errorString = getString(R.string.error_no_secure_decoder,
decoderInitializationException.mimeType);
} else {
errorString = getString(R.string.error_no_decoder,
decoderInitializationException.mimeType);
}
} else {
errorString = getString(R.string.error_instantiating_decoder,
decoderInitializationException.decoderName);
}
}
}
if (errorString != null) {
showToast(errorString);
}
inErrorState = true;
if (isBehindLiveWindow(e)) {
clearStartPosition();
initializePlayer();
@ -753,7 +715,40 @@ public class PlayerActivity extends Activity
lastSeenTrackGroupArray = trackGroups;
}
}
}
private class PlayerErrorMessageProvider implements ErrorMessageProvider<ExoPlaybackException> {
@Override
public Pair<Integer, String> getErrorMessage(ExoPlaybackException e) {
String errorString = getString(R.string.error_generic);
if (e.type == ExoPlaybackException.TYPE_RENDERER) {
Exception cause = e.getRendererException();
if (cause instanceof DecoderInitializationException) {
// Special case for decoder initialization failures.
DecoderInitializationException decoderInitializationException =
(DecoderInitializationException) cause;
if (decoderInitializationException.decoderName == null) {
if (decoderInitializationException.getCause() instanceof DecoderQueryException) {
errorString = getString(R.string.error_querying_decoders);
} else if (decoderInitializationException.secureDecoderRequired) {
errorString =
getString(
R.string.error_no_secure_decoder, decoderInitializationException.mimeType);
} else {
errorString =
getString(R.string.error_no_decoder, decoderInitializationException.mimeType);
}
} else {
errorString =
getString(
R.string.error_instantiating_decoder,
decoderInitializationException.decoderName);
}
}
}
return Pair.create(0, errorString);
}
}
}

View File

@ -19,6 +19,8 @@
<string name="unexpected_intent_action">Unexpected intent action: <xliff:g id="action">%1$s</xliff:g></string>
<string name="error_generic">Playback failed</string>
<string name="error_unrecognized_abr_algorithm">Unrecognized ABR algorithm</string>
<string name="error_drm_not_supported">Protected content not supported on API levels below 18</string>

View File

@ -53,7 +53,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
private @Nullable PlaybackPreparer playbackPreparer;
private ControlDispatcher controlDispatcher;
private ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
private @Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
private SurfaceHolderGlueHost surfaceHolderGlueHost;
private boolean hasSurface;
private boolean lastNotifiedPreparedState;
@ -110,7 +110,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
* @param errorMessageProvider The {@link ErrorMessageProvider}.
*/
public void setErrorMessageProvider(
ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider) {
@Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider) {
this.errorMessageProvider = errorMessageProvider;
}

View File

@ -334,7 +334,7 @@ public final class MediaSessionConnector {
private Player player;
private CustomActionProvider[] customActionProviders;
private Map<String, CustomActionProvider> customActionMap;
private ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
private @Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
private PlaybackPreparer playbackPreparer;
private QueueNavigator queueNavigator;
private QueueEditor queueEditor;
@ -436,12 +436,12 @@ public final class MediaSessionConnector {
}
/**
* Sets the {@link ErrorMessageProvider}.
* Sets the optional {@link ErrorMessageProvider}.
*
* @param errorMessageProvider The error message provider.
*/
public void setErrorMessageProvider(
ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider) {
@Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider) {
if (this.errorMessageProvider != errorMessageProvider) {
this.errorMessageProvider = errorMessageProvider;
updateMediaSessionPlaybackState();

View File

@ -36,9 +36,11 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
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.PlaybackPreparer;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Player.DiscontinuityReason;
@ -51,6 +53,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.ResizeMode;
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 com.google.android.exoplayer2.video.VideoListener;
@ -102,6 +105,12 @@ import java.util.List;
* <li>Corresponding method: {@link #setControllerHideDuringAds(boolean)}
* <li>Default: {@code true}
* </ul>
* <li><b>{@code show_buffering}</b> - Whether the buffering spinner is displayed when the player
* is buffering.
* <ul>
* <li>Corresponding method: {@link #setShowBuffering(boolean)}
* <li>Default: {@code false}
* </ul>
* <li><b>{@code resize_mode}</b> - Controls how video and album art is resized within the view.
* Valid values are {@code fit}, {@code fixed_width}, {@code fixed_height} and {@code fill}.
* <ul>
@ -164,6 +173,11 @@ import java.util.List;
* <ul>
* <li>Type: {@link View}
* </ul>
* <li><b>{@code exo_buffering}</b> - A view that's made visible when the player is buffering.
* This view typically displays a buffering spinner or animation.
* <ul>
* <li>Type: {@link View}
* </ul>
* <li><b>{@code exo_subtitles}</b> - Displays subtitles.
* <ul>
* <li>Type: {@link SubtitleView}
@ -172,6 +186,10 @@ import java.util.List;
* <ul>
* <li>Type: {@link ImageView}
* </ul>
* <li><b>{@code exo_error_message}</b> - Displays an error message to the user if playback fails.
* <ul>
* <li>Type: {@link TextView}
* </ul>
* <li><b>{@code exo_controller_placeholder}</b> - A placeholder that's replaced with the inflated
* {@link PlayerControlView}. Ignored if an {@code exo_controller} view exists.
* <ul>
@ -213,6 +231,8 @@ public class PlayerView extends FrameLayout {
private final View surfaceView;
private final ImageView artworkView;
private final SubtitleView subtitleView;
private final @Nullable View bufferingView;
private final @Nullable TextView errorMessageView;
private final PlayerControlView controller;
private final ComponentListener componentListener;
private final FrameLayout overlayFrameLayout;
@ -221,6 +241,9 @@ public class PlayerView extends FrameLayout {
private boolean useController;
private boolean useArtwork;
private Bitmap defaultArtwork;
private boolean showBuffering;
private @Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
private @Nullable CharSequence customErrorMessage;
private int controllerShowTimeoutMs;
private boolean controllerAutoShow;
private boolean controllerHideDuringAds;
@ -244,6 +267,8 @@ public class PlayerView extends FrameLayout {
surfaceView = null;
artworkView = null;
subtitleView = null;
bufferingView = null;
errorMessageView = null;
controller = null;
componentListener = null;
overlayFrameLayout = null;
@ -269,6 +294,7 @@ public class PlayerView extends FrameLayout {
boolean controllerHideOnTouch = true;
boolean controllerAutoShow = true;
boolean controllerHideDuringAds = true;
boolean showBuffering = false;
if (attrs != null) {
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.PlayerView, 0, 0);
try {
@ -286,6 +312,7 @@ public class PlayerView extends FrameLayout {
controllerHideOnTouch =
a.getBoolean(R.styleable.PlayerView_hide_on_touch, controllerHideOnTouch);
controllerAutoShow = a.getBoolean(R.styleable.PlayerView_auto_show, controllerAutoShow);
showBuffering = a.getBoolean(R.styleable.PlayerView_show_buffering, showBuffering);
controllerHideDuringAds =
a.getBoolean(R.styleable.PlayerView_hide_during_ads, controllerHideDuringAds);
} finally {
@ -341,6 +368,19 @@ public class PlayerView extends FrameLayout {
subtitleView.setUserDefaultTextSize();
}
// Buffering view.
bufferingView = findViewById(R.id.exo_buffering);
if (bufferingView != null) {
bufferingView.setVisibility(View.GONE);
}
this.showBuffering = showBuffering;
// Error message view.
errorMessageView = findViewById(R.id.exo_error_message);
if (errorMessageView != null) {
errorMessageView.setVisibility(View.GONE);
}
// Playback control view.
PlayerControlView customController = findViewById(R.id.exo_controller);
View controllerPlaceholder = findViewById(R.id.exo_controller_placeholder);
@ -438,6 +478,8 @@ public class PlayerView extends FrameLayout {
if (subtitleView != null) {
subtitleView.setCues(null);
}
updateBuffering();
updateErrorMessage();
if (player != null) {
Player.VideoComponent newVideoComponent = player.getVideoComponent();
if (newVideoComponent != null) {
@ -558,6 +600,44 @@ public class PlayerView extends FrameLayout {
}
}
/**
* Sets whether a buffering spinner is displayed when the player is in the buffering state. The
* buffering spinner is not displayed by default.
*
* @param showBuffering Whether the buffering icon is displayer
*/
public void setShowBuffering(boolean showBuffering) {
if (this.showBuffering != showBuffering) {
this.showBuffering = showBuffering;
updateBuffering();
}
}
/**
* Sets the optional {@link ErrorMessageProvider}.
*
* @param errorMessageProvider The error message provider.
*/
public void setErrorMessageProvider(
@Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider) {
if (this.errorMessageProvider != errorMessageProvider) {
this.errorMessageProvider = errorMessageProvider;
updateErrorMessage();
}
}
/**
* Sets a custom error message to be displayed by the view. The error message will be displayed
* permanently, unless it is cleared by passing {@code null} to this method.
*
* @param message The message to display, or {@code null} to clear a previously set message.
*/
public void setCustomErrorMessage(@Nullable CharSequence message) {
Assertions.checkState(errorMessageView != null);
customErrorMessage = message;
updateErrorMessage();
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if (player != null && player.isPlayingAd()) {
@ -954,6 +1034,40 @@ public class PlayerView extends FrameLayout {
}
}
private void updateBuffering() {
if (bufferingView != null) {
boolean showBufferingSpinner =
showBuffering
&& player != null
&& player.getPlaybackState() == Player.STATE_BUFFERING
&& player.getPlayWhenReady();
bufferingView.setVisibility(showBufferingSpinner ? View.VISIBLE : View.GONE);
}
}
private void updateErrorMessage() {
if (errorMessageView != null) {
if (customErrorMessage != null) {
errorMessageView.setText(customErrorMessage);
errorMessageView.setVisibility(View.VISIBLE);
return;
}
ExoPlaybackException error = null;
if (player != null
&& player.getPlaybackState() == Player.STATE_IDLE
&& errorMessageProvider != null) {
error = player.getPlaybackError();
}
if (error != null) {
CharSequence errorMessage = errorMessageProvider.getErrorMessage(error).second;
errorMessageView.setText(errorMessage);
errorMessageView.setVisibility(View.VISIBLE);
} else {
errorMessageView.setVisibility(View.GONE);
}
}
}
@TargetApi(23)
private static void configureEditModeLogoV23(Resources resources, ImageView logo) {
logo.setImageDrawable(resources.getDrawable(R.drawable.exo_edit_mode_logo, null));
@ -1070,6 +1184,8 @@ public class PlayerView extends FrameLayout {
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
updateBuffering();
updateErrorMessage();
if (isPlayingAd() && controllerHideDuringAds) {
hideController();
} else {

View File

@ -36,6 +36,20 @@
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<ProgressBar android:id="@id/exo_buffering"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"
android:layout_gravity="center"/>
<TextView android:id="@id/exo_error_message"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center"
android:background="@color/exo_error_message_background_color"
android:padding="16dp"/>
</com.google.android.exoplayer2.ui.AspectRatioFrameLayout>
<FrameLayout android:id="@id/exo_overlay"

View File

@ -50,6 +50,7 @@
<attr name="hide_on_touch" format="boolean"/>
<attr name="hide_during_ads" format="boolean"/>
<attr name="auto_show" format="boolean"/>
<attr name="show_buffering" format="boolean"/>
<attr name="resize_mode"/>
<attr name="surface_type"/>
<attr name="player_layout_id"/>

View File

@ -18,6 +18,7 @@
<dimen name="exo_media_button_width">71dp</dimen>
<dimen name="exo_media_button_height">52dp</dimen>
<color name="exo_error_message_background_color">#AA000000</color>
<color name="exo_edit_mode_background_color">#FFF4F3F0</color>
</resources>

View File

@ -33,5 +33,7 @@
<item name="exo_duration" type="id"/>
<item name="exo_position" type="id"/>
<item name="exo_progress" type="id"/>
<item name="exo_buffering" type="id"/>
<item name="exo_error_message" type="id"/>
</resources>