-Added better player exception handling to player.

-Added expired media source cleaning to media source manager.
This commit is contained in:
John Zhen Mo 2018-03-27 12:10:48 -07:00
parent 7219c8d33c
commit ece93cadd5
5 changed files with 65 additions and 51 deletions

View File

@ -56,7 +56,8 @@ public final class BookmarkFragment
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final AppDatabase database = NewPipeDatabase.getInstance(getContext());
if (activity == null) return;
final AppDatabase database = NewPipeDatabase.getInstance(activity);
localPlaylistManager = new LocalPlaylistManager(database);
remotePlaylistManager = new RemotePlaylistManager(database);
disposables = new CompositeDisposable();

View File

@ -64,6 +64,7 @@ import org.schabi.newpipe.player.helper.LoadController;
import org.schabi.newpipe.player.helper.MediaSessionManager;
import org.schabi.newpipe.player.helper.PlayerDataSource;
import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.player.mediasource.FailedMediaSource;
import org.schabi.newpipe.player.playback.BasePlayerMediaSession;
import org.schabi.newpipe.player.playback.CustomTrackSelector;
import org.schabi.newpipe.player.playback.MediaSourceManager;
@ -700,26 +701,6 @@ public abstract class BasePlayer implements
}
}
/**
* Processes {@link ExoPlaybackException} tagged with {@link ExoPlaybackException#TYPE_SOURCE}.
* <br><br>
* If the current {@link com.google.android.exoplayer2.Timeline.Window window} is valid,
* then we know the error is produced by transitioning into a bad window, therefore we report
* an error to the play queue based on if the current error can be skipped.
* <br><br>
* 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>
* In the event that this error is produced during a valid stream playback, we save the
* current position so the playback may be recovered and resumed manually by the user. This
* happens only if the playback is {@link #RECOVERY_SKIP_THRESHOLD} milliseconds until complete.
* <br><br>
* In the event of livestreaming being lagged behind for any reason, most notably pausing for
* too long, a {@link BehindLiveWindowException} will be produced. This will trigger a reload
* instead of skipping or removal.
* */
private void processSourceError(final IOException error) {
if (simpleExoPlayer == null || playQueue == null) return;
@ -733,8 +714,14 @@ public abstract class BasePlayer implements
reload();
} else if (cause instanceof UnknownHostException) {
playQueue.error(/*isNetworkProblem=*/true);
} else if (isCurrentWindowValid()) {
playQueue.error(/*isTransitioningToBadStream=*/true);
} else if (error instanceof FailedMediaSource.MediaSourceResolutionException) {
playQueue.error(/*recoverableWithNoAvailableStream=*/false);
} else if (error instanceof FailedMediaSource.StreamInfoLoadException) {
playQueue.error(/*recoverableIfLoadFailsWhenNetworkIsFine=*/false);
} else {
playQueue.error(isCurrentWindowValid());
playQueue.error(/*noIdeaWhatHappenedAndLetUserChooseWhatToDo=*/true);
}
}

View File

@ -14,13 +14,35 @@ import java.io.IOException;
public class FailedMediaSource implements ManagedMediaSource {
private final String TAG = "FailedMediaSource@" + Integer.toHexString(hashCode());
public static class FailedMediaSourceException extends IOException {
FailedMediaSourceException(String message) {
super(message);
}
FailedMediaSourceException(Throwable cause) {
super(cause);
}
}
public static final class MediaSourceResolutionException extends FailedMediaSourceException {
public MediaSourceResolutionException(String message) {
super(message);
}
}
public static final class StreamInfoLoadException extends FailedMediaSourceException {
public StreamInfoLoadException(Throwable cause) {
super(cause);
}
}
private final PlayQueueItem playQueueItem;
private final Throwable error;
private final FailedMediaSourceException error;
private final long retryTimestamp;
public FailedMediaSource(@NonNull final PlayQueueItem playQueueItem,
@NonNull final Throwable error,
@NonNull final FailedMediaSourceException error,
final long retryTimestamp) {
this.playQueueItem = playQueueItem;
this.error = error;
@ -32,7 +54,7 @@ public class FailedMediaSource implements ManagedMediaSource {
* The error will always be propagated to ExoPlayer.
* */
public FailedMediaSource(@NonNull final PlayQueueItem playQueueItem,
@NonNull final Throwable error) {
@NonNull final FailedMediaSourceException error) {
this.playQueueItem = playQueueItem;
this.error = error;
this.retryTimestamp = Long.MAX_VALUE;
@ -42,7 +64,7 @@ public class FailedMediaSource implements ManagedMediaSource {
return playQueueItem;
}
public Throwable getError() {
public FailedMediaSourceException getError() {
return error;
}
@ -57,7 +79,7 @@ public class FailedMediaSource implements ManagedMediaSource {
@Override
public void maybeThrowSourceInfoRefreshError() throws IOException {
throw new IOException(error);
throw error;
}
@Override

View File

@ -129,15 +129,16 @@ public class ManagedMediaSourcePlaylist {
if (index < 0 || index >= internalSource.getSize()) return;
// Add and remove are sequential on the same thread, therefore here, the exoplayer
// message queue must receive and process add before remove.
// message queue must receive and process add before remove, effectively treating them
// as atomic.
// However, finalizing action occurs strictly after the timeline has completed
// all its changes on the playback thread, so it is possible, in the meantime, other calls
// that modifies the playlist media source may occur in between. Therefore,
// it is not safe to call remove as the finalizing action of add.
// Since the finalizing action occurs strictly after the timeline has completed
// all its changes on the playback thread, thus, it is possible, in the meantime,
// other calls that modifies the playlist media source occur in between. This makes
// it unsafe to call remove as the finalizing action of add.
internalSource.addMediaSource(index + 1, source);
// Also, because of the above, it is thus only safe to synchronize the player
// Because of the above race condition, it is thus only safe to synchronize the player
// in the finalizing action AFTER the removal is complete and the timeline has changed.
internalSource.removeMediaSource(index, finalizingAction);
}

View File

@ -2,6 +2,7 @@ package org.schabi.newpipe.player.playback;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.util.ArraySet;
import android.util.Log;
import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource;
@ -24,7 +25,6 @@ import org.schabi.newpipe.playlist.events.ReorderEvent;
import org.schabi.newpipe.util.ServiceHelper;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
@ -39,6 +39,7 @@ import io.reactivex.functions.Consumer;
import io.reactivex.internal.subscriptions.EmptySubscription;
import io.reactivex.subjects.PublishSubject;
import static org.schabi.newpipe.player.mediasource.FailedMediaSource.*;
import static org.schabi.newpipe.playlist.PlayQueue.DEBUG;
public class MediaSourceManager {
@ -144,7 +145,7 @@ public class MediaSourceManager {
this.playlist = new ManagedMediaSourcePlaylist();
this.loadingItems = Collections.synchronizedSet(new HashSet<>());
this.loadingItems = Collections.synchronizedSet(new ArraySet<>());
playQueue.getBroadcastReceiver()
.observeOn(AndroidSchedulers.mainThread())
@ -321,9 +322,9 @@ public class MediaSourceManager {
}
private void maybeSynchronizePlayer() {
cleanSweep();
maybeUnblock();
maybeSync();
cleanPlaylist();
}
/*//////////////////////////////////////////////////////////////////////////
@ -366,7 +367,7 @@ public class MediaSourceManager {
final int leftBound = Math.max(0, currentIndex - WINDOW_SIZE);
final int rightLimit = currentIndex + WINDOW_SIZE + 1;
final int rightBound = Math.min(playQueue.size(), rightLimit);
final Set<PlayQueueItem> items = new HashSet<>(
final Set<PlayQueueItem> items = new ArraySet<>(
playQueue.getStreams().subList(leftBound,rightBound));
// Do a round robin
@ -402,19 +403,19 @@ public class MediaSourceManager {
return stream.getStream().map(streamInfo -> {
final MediaSource source = playbackListener.sourceOf(stream, streamInfo);
if (source == null) {
final Exception exception = new IllegalStateException(
"Unable to resolve source from stream info." +
" URL: " + stream.getUrl() +
", audio count: " + streamInfo.getAudioStreams().size() +
", video count: " + streamInfo.getVideoOnlyStreams().size() +
streamInfo.getVideoStreams().size());
return new FailedMediaSource(stream, exception);
final String message = "Unable to resolve source from stream info." +
" URL: " + stream.getUrl() +
", audio count: " + streamInfo.getAudioStreams().size() +
", video count: " + streamInfo.getVideoOnlyStreams().size() +
streamInfo.getVideoStreams().size();
return new FailedMediaSource(stream, new MediaSourceResolutionException(message));
}
final long expiration = System.currentTimeMillis() +
ServiceHelper.getCacheExpirationMillis(streamInfo.getServiceId());
return new LoadedMediaSource(source, stream, expiration);
}).onErrorReturn(throwable -> new FailedMediaSource(stream, throwable));
}).onErrorReturn(throwable -> new FailedMediaSource(stream,
new StreamInfoLoadException(throwable)));
}
private void onMediaSourceReceived(@NonNull final PlayQueueItem item,
@ -478,13 +479,15 @@ public class MediaSourceManager {
}
/**
* Scans the entire playlist for {@link MediaSource}s that requires correction,
* and replace these sources with a {@link PlaceholderMediaSource}.
* Scans the entire playlist for {@link ManagedMediaSource}s that requires correction,
* and replaces these sources with a {@link PlaceholderMediaSource} if they are not part
* of the excluded items.
* */
private void cleanSweep() {
for (int index = 0; index < playlist.size(); index++) {
if (isCorrectionNeeded(playQueue.getItem(index))) {
playlist.invalidate(index);
private void cleanPlaylist() {
if (DEBUG) Log.d(TAG, "cleanPlaylist() called.");
for (final PlayQueueItem item : playQueue.getStreams()) {
if (isCorrectionNeeded(item)) {
playlist.invalidate(playQueue.indexOf(item));
}
}
}