-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 @Override
public void onError(Exception exception) { public void onRecoverableError(Exception exception) {
exception.printStackTrace(); exception.printStackTrace();
Toast.makeText(context, "Failed to play this audio", Toast.LENGTH_SHORT).show(); 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 // 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 @Override
public void onPlayerError(ExoPlaybackException error) { public void onPlayerError(ExoPlaybackException error) {
if (DEBUG) Log.d(TAG, "onPlayerError() called with: error = [" + 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 switch (error.type) {
// bad window, therefore we simply increment the current index. case ExoPlaybackException.TYPE_SOURCE:
// This is done because ExoPlayer reports the exception before window is if (simpleExoPlayer.getDuration() < 0 || simpleExoPlayer.getCurrentPosition() < 0) {
// transitioned due to seamless playback.
if (!simpleExoPlayer.isCurrentWindowSeekable()) {
playQueue.error(); playQueue.error();
onError(error); onRecoverableError(error);
} else { } else {
playQueue.offsetIndex(+1); playQueue.offsetIndex(+1);
} }
// Player error causes ExoPlayer to go back to IDLE state, which requires resetting
// preparing a new media source.
playbackManager.reset(); playbackManager.reset();
playbackManager.load(); playbackManager.load();
break;
default:
onUnrecoverableError(error);
break;
}
} }
@Override @Override
@ -752,7 +775,9 @@ public abstract class BasePlayer implements Player.EventListener,
// General Player // 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) { public void onPrepared(boolean playWhenReady) {
if (DEBUG) Log.d(TAG, "onPrepared() called with: playWhenReady = [" + playWhenReady + "]"); if (DEBUG) Log.d(TAG, "onPrepared() called with: playWhenReady = [" + playWhenReady + "]");

View File

@ -352,11 +352,18 @@ public final class MainVideoPlayer extends Activity {
} }
@Override @Override
public void onError(Exception exception) { public void onRecoverableError(Exception exception) {
exception.printStackTrace(); exception.printStackTrace();
Toast.makeText(context, "Failed to play this video", Toast.LENGTH_SHORT).show(); 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 // States
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/

View File

@ -463,11 +463,18 @@ public final class PopupVideoPlayer extends Service {
} }
@Override @Override
public void onError(Exception exception) { public void onRecoverableError(Exception exception) {
exception.printStackTrace(); exception.printStackTrace();
Toast.makeText(context, "Failed to play this video", Toast.LENGTH_SHORT).show(); 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 @Override
public void onStopTrackingTouch(SeekBar seekBar) { public void onStopTrackingTouch(SeekBar seekBar) {
super.onStopTrackingTouch(seekBar); super.onStopTrackingTouch(seekBar);

View File

@ -16,6 +16,7 @@ import java.io.IOException;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable; import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Consumer; import io.reactivex.functions.Consumer;
import io.reactivex.functions.Function;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
/** /**
@ -113,10 +114,17 @@ public final class DeferredMediaSource implements MediaSource {
Log.d(TAG, "Loading: [" + stream.getTitle() + "] with url: " + stream.getUrl()); 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 @Override
public void accept(StreamInfo streamInfo) throws Exception { public MediaSource apply(StreamInfo streamInfo) throws Exception {
onStreamInfoReceived(streamInfo); return onStreamInfoReceived(streamInfo);
}
};
final Consumer<MediaSource> onSuccess = new Consumer<MediaSource>() {
@Override
public void accept(MediaSource mediaSource) throws Exception {
onMediaSourceReceived(mediaSource);
} }
}; };
@ -128,29 +136,38 @@ public final class DeferredMediaSource implements MediaSource {
}; };
loader = stream.getStream() loader = stream.getStream()
.subscribeOn(Schedulers.io()) .observeOn(Schedulers.io())
.map(onReceive)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(onSuccess, onError); .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()); Log.d(TAG, " Loaded: [" + stream.getTitle() + "] with url: " + stream.getUrl());
state = STATE_LOADED; state = STATE_LOADED;
if (exoPlayer == null || listener == null || streamInfo == null) { this.mediaSource = mediaSource;
error = new Throwable("Stream info loading failed. URL: " + stream.getUrl()); this.mediaSource.prepareSource(exoPlayer, false, listener);
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);
} }
private void onStreamInfoError(final Throwable throwable) { private void onStreamInfoError(final Throwable throwable) {