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) {