-Added better player exception handling to player.
-Added expired media source cleaning to media source manager.
This commit is contained in:
parent
7219c8d33c
commit
ece93cadd5
5 changed files with 65 additions and 51 deletions
|
@ -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();
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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, new MediaSourceResolutionException(message));
|
||||||
return new FailedMediaSource(stream, exception);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue