mirror of
				https://github.com/TeamNewPipe/NewPipe
				synced 2025-10-31 07:13:00 +00:00 
			
		
		
		
	-Added better player exception handling to player.
-Added expired media source cleaning to media source manager.
This commit is contained in:
		| @@ -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, 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)); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 John Zhen Mo
					John Zhen Mo