diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 7e031c30bf..b9913b94e7 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -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: diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 684a7fd4c7..cdbe3e3f88 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -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,16 +236,16 @@ public class PlayerActivity extends Activity @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - if (grantResults.length > 0) { - if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { - initializePlayer(); - } else { - showToast(R.string.storage_permission_denied); - finish(); - } - } else { + 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(); } } @@ -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 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 { + + @Override + public Pair 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); + } } } diff --git a/demos/main/src/main/res/values/strings.xml b/demos/main/src/main/res/values/strings.xml index 57f7736868..1164af8cf5 100644 --- a/demos/main/src/main/res/values/strings.xml +++ b/demos/main/src/main/res/values/strings.xml @@ -19,6 +19,8 @@ Unexpected intent action: %1$s + Playback failed + Unrecognized ABR algorithm Protected content not supported on API levels below 18 diff --git a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java index e513084974..03f53c263f 100644 --- a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java +++ b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java @@ -53,7 +53,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { private @Nullable PlaybackPreparer playbackPreparer; private ControlDispatcher controlDispatcher; - private ErrorMessageProvider errorMessageProvider; + private @Nullable ErrorMessageProvider 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 errorMessageProvider) { + @Nullable ErrorMessageProvider errorMessageProvider) { this.errorMessageProvider = errorMessageProvider; } diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index 3c40b359b8..83fb16236d 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -334,7 +334,7 @@ public final class MediaSessionConnector { private Player player; private CustomActionProvider[] customActionProviders; private Map customActionMap; - private ErrorMessageProvider errorMessageProvider; + private @Nullable ErrorMessageProvider 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 errorMessageProvider) { + @Nullable ErrorMessageProvider errorMessageProvider) { if (this.errorMessageProvider != errorMessageProvider) { this.errorMessageProvider = errorMessageProvider; updateMediaSessionPlaybackState(); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java index 6732f755d6..25c4318768 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java @@ -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; *
  • Corresponding method: {@link #setControllerHideDuringAds(boolean)} *
  • Default: {@code true} * + *
  • {@code show_buffering} - Whether the buffering spinner is displayed when the player + * is buffering. + *
      + *
    • Corresponding method: {@link #setShowBuffering(boolean)} + *
    • Default: {@code false} + *
    *
  • {@code resize_mode} - 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}. *
      @@ -164,6 +173,11 @@ import java.util.List; *
        *
      • Type: {@link View} *
      + *
    • {@code exo_buffering} - A view that's made visible when the player is buffering. + * This view typically displays a buffering spinner or animation. + *
        + *
      • Type: {@link View} + *
      *
    • {@code exo_subtitles} - Displays subtitles. *
        *
      • Type: {@link SubtitleView} @@ -172,6 +186,10 @@ import java.util.List; *
          *
        • Type: {@link ImageView} *
        + *
      • {@code exo_error_message} - Displays an error message to the user if playback fails. + *
          + *
        • Type: {@link TextView} + *
        *
      • {@code exo_controller_placeholder} - A placeholder that's replaced with the inflated * {@link PlayerControlView}. Ignored if an {@code exo_controller} view exists. *
          @@ -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 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 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 { diff --git a/library/ui/src/main/res/layout/exo_simple_player_view.xml b/library/ui/src/main/res/layout/exo_simple_player_view.xml index 340113da6c..167ac96222 100644 --- a/library/ui/src/main/res/layout/exo_simple_player_view.xml +++ b/library/ui/src/main/res/layout/exo_simple_player_view.xml @@ -36,6 +36,20 @@ android:layout_width="match_parent" android:layout_height="match_parent"/> + + + + + diff --git a/library/ui/src/main/res/values/constants.xml b/library/ui/src/main/res/values/constants.xml index eb94cacadd..9b374d8382 100644 --- a/library/ui/src/main/res/values/constants.xml +++ b/library/ui/src/main/res/values/constants.xml @@ -18,6 +18,7 @@ 71dp 52dp + #AA000000 #FFF4F3F0 diff --git a/library/ui/src/main/res/values/ids.xml b/library/ui/src/main/res/values/ids.xml index b90d2329b3..184e51ac58 100644 --- a/library/ui/src/main/res/values/ids.xml +++ b/library/ui/src/main/res/values/ids.xml @@ -33,5 +33,7 @@ + +