1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2024-11-17 15:24:55 +00:00

-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 @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
final AppDatabase database = NewPipeDatabase.getInstance(getContext()); if (activity == null) return;
final AppDatabase database = NewPipeDatabase.getInstance(activity);
localPlaylistManager = new LocalPlaylistManager(database); localPlaylistManager = new LocalPlaylistManager(database);
remotePlaylistManager = new RemotePlaylistManager(database); remotePlaylistManager = new RemotePlaylistManager(database);
disposables = new CompositeDisposable(); 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.MediaSessionManager;
import org.schabi.newpipe.player.helper.PlayerDataSource; import org.schabi.newpipe.player.helper.PlayerDataSource;
import org.schabi.newpipe.player.helper.PlayerHelper; 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.BasePlayerMediaSession;
import org.schabi.newpipe.player.playback.CustomTrackSelector; import org.schabi.newpipe.player.playback.CustomTrackSelector;
import org.schabi.newpipe.player.playback.MediaSourceManager; 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) { private void processSourceError(final IOException error) {
if (simpleExoPlayer == null || playQueue == null) return; if (simpleExoPlayer == null || playQueue == null) return;
@ -733,8 +714,14 @@ public abstract class BasePlayer implements
reload(); reload();
} else if (cause instanceof UnknownHostException) { } else if (cause instanceof UnknownHostException) {
playQueue.error(/*isNetworkProblem=*/true); 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 { } else {
playQueue.error(isCurrentWindowValid()); playQueue.error(/*noIdeaWhatHappenedAndLetUserChooseWhatToDo=*/true);
} }
} }

View File

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

View File

@ -129,15 +129,16 @@ public class ManagedMediaSourcePlaylist {
if (index < 0 || index >= internalSource.getSize()) return; if (index < 0 || index >= internalSource.getSize()) return;
// Add and remove are sequential on the same thread, therefore here, the exoplayer // 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 // Since the 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 // all its changes on the playback thread, thus, it is possible, in the meantime,
// that modifies the playlist media source may occur in between. Therefore, // other calls that modifies the playlist media source occur in between. This makes
// it is not safe to call remove as the finalizing action of add. // it unsafe to call remove as the finalizing action of add.
internalSource.addMediaSource(index + 1, source); 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. // in the finalizing action AFTER the removal is complete and the timeline has changed.
internalSource.removeMediaSource(index, finalizingAction); 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.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v4.util.ArraySet;
import android.util.Log; import android.util.Log;
import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource; 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 org.schabi.newpipe.util.ServiceHelper;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
@ -39,6 +39,7 @@ import io.reactivex.functions.Consumer;
import io.reactivex.internal.subscriptions.EmptySubscription; import io.reactivex.internal.subscriptions.EmptySubscription;
import io.reactivex.subjects.PublishSubject; import io.reactivex.subjects.PublishSubject;
import static org.schabi.newpipe.player.mediasource.FailedMediaSource.*;
import static org.schabi.newpipe.playlist.PlayQueue.DEBUG; import static org.schabi.newpipe.playlist.PlayQueue.DEBUG;
public class MediaSourceManager { public class MediaSourceManager {
@ -144,7 +145,7 @@ public class MediaSourceManager {
this.playlist = new ManagedMediaSourcePlaylist(); this.playlist = new ManagedMediaSourcePlaylist();
this.loadingItems = Collections.synchronizedSet(new HashSet<>()); this.loadingItems = Collections.synchronizedSet(new ArraySet<>());
playQueue.getBroadcastReceiver() playQueue.getBroadcastReceiver()
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
@ -321,9 +322,9 @@ public class MediaSourceManager {
} }
private void maybeSynchronizePlayer() { private void maybeSynchronizePlayer() {
cleanSweep();
maybeUnblock(); maybeUnblock();
maybeSync(); maybeSync();
cleanPlaylist();
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
@ -366,7 +367,7 @@ public class MediaSourceManager {
final int leftBound = Math.max(0, currentIndex - WINDOW_SIZE); final int leftBound = Math.max(0, currentIndex - WINDOW_SIZE);
final int rightLimit = currentIndex + WINDOW_SIZE + 1; final int rightLimit = currentIndex + WINDOW_SIZE + 1;
final int rightBound = Math.min(playQueue.size(), rightLimit); 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)); playQueue.getStreams().subList(leftBound,rightBound));
// Do a round robin // Do a round robin
@ -402,19 +403,19 @@ public class MediaSourceManager {
return stream.getStream().map(streamInfo -> { return stream.getStream().map(streamInfo -> {
final MediaSource source = playbackListener.sourceOf(stream, streamInfo); final MediaSource source = playbackListener.sourceOf(stream, streamInfo);
if (source == null) { if (source == null) {
final Exception exception = new IllegalStateException( final String message = "Unable to resolve source from stream info." +
"Unable to resolve source from stream info." +
" URL: " + stream.getUrl() + " URL: " + stream.getUrl() +
", audio count: " + streamInfo.getAudioStreams().size() + ", audio count: " + streamInfo.getAudioStreams().size() +
", video count: " + streamInfo.getVideoOnlyStreams().size() + ", video count: " + streamInfo.getVideoOnlyStreams().size() +
streamInfo.getVideoStreams().size()); streamInfo.getVideoStreams().size();
return new FailedMediaSource(stream, exception); return new FailedMediaSource(stream, new MediaSourceResolutionException(message));
} }
final long expiration = System.currentTimeMillis() + final long expiration = System.currentTimeMillis() +
ServiceHelper.getCacheExpirationMillis(streamInfo.getServiceId()); ServiceHelper.getCacheExpirationMillis(streamInfo.getServiceId());
return new LoadedMediaSource(source, stream, expiration); 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, 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, * Scans the entire playlist for {@link ManagedMediaSource}s that requires correction,
* and replace these sources with a {@link PlaceholderMediaSource}. * and replaces these sources with a {@link PlaceholderMediaSource} if they are not part
* of the excluded items.
* */ * */
private void cleanSweep() { private void cleanPlaylist() {
for (int index = 0; index < playlist.size(); index++) { if (DEBUG) Log.d(TAG, "cleanPlaylist() called.");
if (isCorrectionNeeded(playQueue.getItem(index))) { for (final PlayQueueItem item : playQueue.getStreams()) {
playlist.invalidate(index); if (isCorrectionNeeded(item)) {
playlist.invalidate(playQueue.indexOf(item));
} }
} }
} }