From f5b5982e1c65f4338fbb5f6edc80d84f25066684 Mon Sep 17 00:00:00 2001 From: John Zhen M Date: Sun, 8 Oct 2017 13:15:03 -0700 Subject: [PATCH] -Improved DeferredMediaSource to build source on IO thread. -Improved exception handling for player. --- .../newpipe/player/BackgroundPlayer.java | 9 ++- .../org/schabi/newpipe/player/BasePlayer.java | 55 +++++++++++++----- .../newpipe/player/MainVideoPlayer.java | 9 ++- .../newpipe/player/PopupVideoPlayer.java | 9 ++- .../mediasource/DeferredMediaSource.java | 57 ++++++++++++------- 5 files changed, 101 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java index 9585edfce..03bc734db 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java @@ -401,11 +401,18 @@ public final class BackgroundPlayer extends Service { } @Override - public void onError(Exception exception) { + public void onRecoverableError(Exception exception) { exception.printStackTrace(); Toast.makeText(context, "Failed to play this audio", Toast.LENGTH_SHORT).show(); } + @Override + public void onUnrecoverableError(Exception exception) { + exception.printStackTrace(); + Toast.makeText(context, "Unexpected error occurred", Toast.LENGTH_SHORT).show(); + shutdown(); + } + /*////////////////////////////////////////////////////////////////////////// // ExoPlayer Listener //////////////////////////////////////////////////////////////////////////*/ diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java index 6b6dbbba1..3607a2770 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -661,25 +661,48 @@ public abstract class BasePlayer implements Player.EventListener, } } + /** + * Processes the exceptions produced by {@link com.google.android.exoplayer2.ExoPlayer ExoPlayer}. + * There are multiple types of errors:

+ * + * {@link ExoPlaybackException#TYPE_SOURCE TYPE_SOURCE}:

+ * If the current {@link com.google.android.exoplayer2.Timeline.Window window} has + * duration and position greater than 0, then we know the current window is working correctly + * and the error is produced by transitioning into a bad window, therefore we simply increment + * the current index. Otherwise, we report an error to the play queue. + * + * This is done because ExoPlayer reports the source exceptions before window is + * transitioned on seamless playback. + * + * Because player error causes ExoPlayer to go back to {@link Player#STATE_IDLE STATE_IDLE}, + * we reset and prepare the media source again to resume playback.

+ * + * {@link ExoPlaybackException#TYPE_RENDERER TYPE_RENDERER} and + * {@link ExoPlaybackException#TYPE_UNEXPECTED TYPE_UNEXPECTED}:

+ * If renderer failed or unexpected exceptions occurred, treat the error as unrecoverable. + * + * @see Player.EventListener#onPlayerError(ExoPlaybackException) + * */ @Override public void onPlayerError(ExoPlaybackException error) { if (DEBUG) Log.d(TAG, "onPlayerError() called with: error = [" + error + "]"); - // If the current window is seekable, then the error is produced by transitioning into - // bad window, therefore we simply increment the current index. - // This is done because ExoPlayer reports the exception before window is - // transitioned due to seamless playback. - if (!simpleExoPlayer.isCurrentWindowSeekable()) { - playQueue.error(); - onError(error); - } else { - playQueue.offsetIndex(+1); - } + switch (error.type) { + case ExoPlaybackException.TYPE_SOURCE: + if (simpleExoPlayer.getDuration() < 0 || simpleExoPlayer.getCurrentPosition() < 0) { + playQueue.error(); + onRecoverableError(error); + } else { + playQueue.offsetIndex(+1); + } - // Player error causes ExoPlayer to go back to IDLE state, which requires resetting - // preparing a new media source. - playbackManager.reset(); - playbackManager.load(); + playbackManager.reset(); + playbackManager.load(); + break; + default: + onUnrecoverableError(error); + break; + } } @Override @@ -752,7 +775,9 @@ public abstract class BasePlayer implements Player.EventListener, // General Player //////////////////////////////////////////////////////////////////////////*/ - public abstract void onError(Exception exception); + public abstract void onRecoverableError(Exception exception); + + public abstract void onUnrecoverableError(Exception exception); public void onPrepared(boolean playWhenReady) { if (DEBUG) Log.d(TAG, "onPrepared() called with: playWhenReady = [" + playWhenReady + "]"); diff --git a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java index 9848ddf07..d720c8c61 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -352,11 +352,18 @@ public final class MainVideoPlayer extends Activity { } @Override - public void onError(Exception exception) { + public void onRecoverableError(Exception exception) { exception.printStackTrace(); Toast.makeText(context, "Failed to play this video", Toast.LENGTH_SHORT).show(); } + @Override + public void onUnrecoverableError(Exception exception) { + exception.printStackTrace(); + Toast.makeText(context, "Unexpected error occurred", Toast.LENGTH_SHORT).show(); + shutdown(); + } + /*////////////////////////////////////////////////////////////////////////// // States //////////////////////////////////////////////////////////////////////////*/ diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java index 9706fade5..1437986e2 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -463,11 +463,18 @@ public final class PopupVideoPlayer extends Service { } @Override - public void onError(Exception exception) { + public void onRecoverableError(Exception exception) { exception.printStackTrace(); Toast.makeText(context, "Failed to play this video", Toast.LENGTH_SHORT).show(); } + @Override + public void onUnrecoverableError(Exception exception) { + exception.printStackTrace(); + Toast.makeText(context, "Unexpected error occurred", Toast.LENGTH_SHORT).show(); + shutdown(); + } + @Override public void onStopTrackingTouch(SeekBar seekBar) { super.onStopTrackingTouch(seekBar); diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/DeferredMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/DeferredMediaSource.java index 67279091f..eb6d0da82 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/DeferredMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/DeferredMediaSource.java @@ -16,6 +16,7 @@ import java.io.IOException; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.functions.Consumer; +import io.reactivex.functions.Function; import io.reactivex.schedulers.Schedulers; /** @@ -113,44 +114,60 @@ public final class DeferredMediaSource implements MediaSource { Log.d(TAG, "Loading: [" + stream.getTitle() + "] with url: " + stream.getUrl()); - final Consumer onSuccess = new Consumer() { + final Function onReceive = new Function() { @Override - public void accept(StreamInfo streamInfo) throws Exception { - onStreamInfoReceived(streamInfo); + public MediaSource apply(StreamInfo streamInfo) throws Exception { + return onStreamInfoReceived(streamInfo); + } + }; + + final Consumer onSuccess = new Consumer() { + @Override + public void accept(MediaSource mediaSource) throws Exception { + onMediaSourceReceived(mediaSource); } }; final Consumer onError = new Consumer() { @Override public void accept(Throwable throwable) throws Exception { - onStreamInfoError(throwable); + onStreamInfoError(throwable); } }; loader = stream.getStream() - .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.io()) + .map(onReceive) .observeOn(AndroidSchedulers.mainThread()) .subscribe(onSuccess, onError); } - private void onStreamInfoReceived(final StreamInfo streamInfo) { + private MediaSource onStreamInfoReceived(final StreamInfo streamInfo) throws Exception { + if (callback == null) { + throw new Exception("No available callback for resolving stream info."); + } + + final MediaSource mediaSource = callback.sourceOf(streamInfo); + + if (mediaSource == null) { + throw new Exception("Unable to resolve source from stream info. URL: " + stream.getUrl() + + ", audio count: " + streamInfo.audio_streams.size() + + ", video count: " + streamInfo.video_only_streams.size() + streamInfo.video_streams.size()); + } + + return mediaSource; + } + + private void onMediaSourceReceived(final MediaSource mediaSource) throws Exception { + if (exoPlayer == null || listener == null || mediaSource == null) { + throw new Exception("MediaSource loading failed. URL: " + stream.getUrl()); + } + Log.d(TAG, " Loaded: [" + stream.getTitle() + "] with url: " + stream.getUrl()); state = STATE_LOADED; - if (exoPlayer == null || listener == null || streamInfo == null) { - error = new Throwable("Stream info loading failed. URL: " + stream.getUrl()); - return; - } - - mediaSource = callback.sourceOf(streamInfo); - if (mediaSource == null) { - error = new Throwable("Unable to resolve source from stream info. URL: " + stream.getUrl() + - ", audio count: " + streamInfo.audio_streams.size() + - ", video count: " + streamInfo.video_only_streams.size() + streamInfo.video_streams.size()); - return; - } - - mediaSource.prepareSource(exoPlayer, false, listener); + this.mediaSource = mediaSource; + this.mediaSource.prepareSource(exoPlayer, false, listener); } private void onStreamInfoError(final Throwable throwable) {