-Improved DeferredMediaSource to build source on IO thread.

-Improved exception handling for player.
This commit is contained in:
John Zhen M 2017-10-08 13:15:03 -07:00 committed by John Zhen Mo
parent eebd83d6ac
commit f5b5982e1c
5 changed files with 101 additions and 38 deletions

View file

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

View file

@ -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: <br><br>
*
* {@link ExoPlaybackException#TYPE_SOURCE TYPE_SOURCE}: <br><br>
* 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.<br><br>
*
* {@link ExoPlaybackException#TYPE_RENDERER TYPE_RENDERER} and
* {@link ExoPlaybackException#TYPE_UNEXPECTED TYPE_UNEXPECTED}: <br><br>
* 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 + "]");

View file

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

View file

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

View file

@ -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<StreamInfo> onSuccess = new Consumer<StreamInfo>() {
final Function<StreamInfo, MediaSource> onReceive = new Function<StreamInfo, MediaSource>() {
@Override
public void accept(StreamInfo streamInfo) throws Exception {
onStreamInfoReceived(streamInfo);
public MediaSource apply(StreamInfo streamInfo) throws Exception {
return onStreamInfoReceived(streamInfo);
}
};
final Consumer<MediaSource> onSuccess = new Consumer<MediaSource>() {
@Override
public void accept(MediaSource mediaSource) throws Exception {
onMediaSourceReceived(mediaSource);
}
};
final Consumer<Throwable> onError = new Consumer<Throwable>() {
@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) {