From 4e459b33839e428c26c5730d629ba07356ca88e3 Mon Sep 17 00:00:00 2001 From: karyogamy Date: Sun, 13 Mar 2022 00:22:47 -0500 Subject: [PATCH 1/5] updated: ExoPlayer to 2.17.1. added: MediaItemTag for ManagedMediaSources. added: silent track for FailedMediaSource. added: keyframe fast forward at initial playback buffer. added: error notification on silently skipped streams. --- app/build.gradle | 2 +- .../fragments/detail/VideoDetailFragment.java | 7 +- .../detail/VideoDetailPlayerCrasher.java | 17 +- .../org/schabi/newpipe/player/Player.java | 450 +++++++++--------- .../event/PlayerServiceEventListener.java | 4 +- .../newpipe/player/helper/AudioReactor.java | 6 +- .../newpipe/player/helper/CacheFactory.java | 18 +- .../player/helper/MediaSessionManager.java | 2 - .../player/helper/PlayerDataSource.java | 15 +- .../newpipe/player/helper/PlayerHolder.java | 7 +- .../player/mediaitem/ExceptionTag.java | 99 ++++ .../player/mediaitem/MediaItemTag.java | 113 +++++ .../player/mediaitem/PlaceholderTag.java | 89 ++++ .../player/mediaitem/StreamInfoTag.java | 105 ++++ .../mediasession/PlayQueueNavigator.java | 20 +- .../PlayQueuePlaybackController.java | 23 - .../player/mediasource/FailedMediaSource.java | 109 +++-- .../player/mediasource/LoadedMediaSource.java | 100 +--- .../mediasource/ManagedMediaSource.java | 7 - .../ManagedMediaSourcePlaylist.java | 18 +- .../mediasource/PlaceholderMediaSource.java | 33 +- .../player/playback/CustomTrackSelector.java | 92 ---- .../player/playback/MediaSourceManager.java | 52 +- .../player/playback/PlaybackListener.java | 5 +- .../playback/SurfaceHolderCallback.java | 6 +- .../resolver/AudioPlaybackResolver.java | 4 +- .../player/resolver/MediaSourceTag.java | 53 --- .../player/resolver/PlaybackResolver.java | 27 +- .../resolver/VideoPlaybackResolver.java | 22 +- 29 files changed, 892 insertions(+), 613 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/player/mediaitem/ExceptionTag.java create mode 100644 app/src/main/java/org/schabi/newpipe/player/mediaitem/MediaItemTag.java create mode 100644 app/src/main/java/org/schabi/newpipe/player/mediaitem/PlaceholderTag.java create mode 100644 app/src/main/java/org/schabi/newpipe/player/mediaitem/StreamInfoTag.java delete mode 100644 app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueuePlaybackController.java delete mode 100644 app/src/main/java/org/schabi/newpipe/player/playback/CustomTrackSelector.java delete mode 100644 app/src/main/java/org/schabi/newpipe/player/resolver/MediaSourceTag.java diff --git a/app/build.gradle b/app/build.gradle index 0299c0fd5..929a7820d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -104,7 +104,7 @@ ext { androidxRoomVersion = '2.4.2' icepickVersion = '3.2.0' - exoPlayerVersion = '2.14.2' + exoPlayerVersion = '2.17.1' googleAutoServiceVersion = '1.0.1' groupieVersion = '2.10.0' markwonVersion = '4.6.2' diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 0af5ec99e..c57942aa5 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -43,7 +43,7 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.core.content.ContextCompat; import androidx.preference.PreferenceManager; -import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.PlaybackException; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.material.appbar.AppBarLayout; import com.google.android.material.bottomsheet.BottomSheetBehavior; @@ -1884,9 +1884,8 @@ public final class VideoDetailFragment } @Override - public void onPlayerError(final ExoPlaybackException error) { - if (error.type == ExoPlaybackException.TYPE_SOURCE - || error.type == ExoPlaybackException.TYPE_UNEXPECTED) { + public void onPlayerError(final PlaybackException error, final boolean isCatchableException) { + if (!isCatchableException) { // Properly exit from fullscreen toggleFullscreenIfInFullscreenMode(); hideMainPlayerOnLoadingNewStream(); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailPlayerCrasher.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailPlayerCrasher.java index 9309a8a49..ae704e88c 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailPlayerCrasher.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailPlayerCrasher.java @@ -15,6 +15,7 @@ import androidx.appcompat.app.AlertDialog; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.PlaybackException; import org.schabi.newpipe.R; import org.schabi.newpipe.databinding.ListRadioIconItemBinding; @@ -28,6 +29,10 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.function.Supplier; +import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW; +import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_DECODING_FAILED; +import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_UNSPECIFIED; + /** * Outsourced logic for crashing the player in the {@link VideoDetailFragment}. */ @@ -51,7 +56,8 @@ public final class VideoDetailPlayerCrasher { exceptionTypes.put( "Source", () -> ExoPlaybackException.createForSource( - new IOException(defaultMsg) + new IOException(defaultMsg), + ERROR_CODE_BEHIND_LIVE_WINDOW ) ); exceptionTypes.put( @@ -61,13 +67,16 @@ public final class VideoDetailPlayerCrasher { "Dummy renderer", 0, null, - C.FORMAT_HANDLED + C.FORMAT_HANDLED, + /*isRecoverable=*/false, + ERROR_CODE_DECODING_FAILED ) ); exceptionTypes.put( "Unexpected", () -> ExoPlaybackException.createForUnexpected( - new RuntimeException(defaultMsg) + new RuntimeException(defaultMsg), + ERROR_CODE_UNSPECIFIED ) ); exceptionTypes.put( @@ -139,7 +148,7 @@ public final class VideoDetailPlayerCrasher { /** * Note that this method does not crash the underlying exoplayer directly (it's not possible). - * It simply supplies a Exception to {@link Player#onPlayerError(ExoPlaybackException)}. + * It simply supplies a Exception to {@link Player#onPlayerError(PlaybackException)}. * @param player * @param exception */ diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java index 1051f678f..2305eb9d0 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -1,5 +1,21 @@ package org.schabi.newpipe.player; +import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW; +import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS; +import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED; +import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_IO_FILE_NOT_FOUND; +import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE; +import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED; +import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT; +import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_IO_NO_PERMISSION; +import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE; +import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_IO_UNSPECIFIED; +import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_PARSING_CONTAINER_MALFORMED; +import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED; +import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_PARSING_MANIFEST_MALFORMED; +import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_PARSING_MANIFEST_UNSUPPORTED; +import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_TIMEOUT; +import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_UNSPECIFIED; import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_AUTO_TRANSITION; import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_INTERNAL; import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_REMOVE; @@ -112,20 +128,20 @@ import androidx.recyclerview.widget.RecyclerView; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.DefaultRenderersFactory; -import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.PlaybackException; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player.PositionInfo; import com.google.android.exoplayer2.RenderersFactory; -import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.BehindLiveWindowException; +import com.google.android.exoplayer2.TracksInfo; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import com.google.android.exoplayer2.ui.CaptionStyleCompat; import com.google.android.exoplayer2.ui.SubtitleView; @@ -145,6 +161,7 @@ import org.schabi.newpipe.databinding.PlayerPopupCloseOverlayBinding; import org.schabi.newpipe.error.ErrorInfo; import org.schabi.newpipe.error.ErrorUtil; import org.schabi.newpipe.error.UserAction; +import org.schabi.newpipe.extractor.Info; import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamSegment; @@ -168,7 +185,7 @@ import org.schabi.newpipe.player.helper.PlayerDataSource; import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.player.listeners.view.PlaybackSpeedClickListener; import org.schabi.newpipe.player.listeners.view.QualityClickListener; -import org.schabi.newpipe.player.playback.CustomTrackSelector; +import org.schabi.newpipe.player.mediaitem.MediaItemTag; import org.schabi.newpipe.player.playback.MediaSourceManager; import org.schabi.newpipe.player.playback.PlaybackListener; import org.schabi.newpipe.player.playback.PlayerMediaSession; @@ -180,7 +197,6 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItemBuilder; import org.schabi.newpipe.player.playqueue.PlayQueueItemHolder; import org.schabi.newpipe.player.playqueue.PlayQueueItemTouchCallback; import org.schabi.newpipe.player.resolver.AudioPlaybackResolver; -import org.schabi.newpipe.player.resolver.MediaSourceTag; import org.schabi.newpipe.player.resolver.VideoPlaybackResolver; import org.schabi.newpipe.player.resolver.VideoPlaybackResolver.SourceType; import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper; @@ -196,8 +212,8 @@ import org.schabi.newpipe.util.external_communication.ShareUtils; import org.schabi.newpipe.views.ExpandableSurfaceView; import org.schabi.newpipe.views.player.PlayerFastSeekOverlay; -import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -278,19 +294,19 @@ public final class Player implements @Nullable private MediaSourceManager playQueueManager; @Nullable private PlayQueueItem currentItem; - @Nullable private MediaSourceTag currentMetadata; + @Nullable private MediaItemTag currentMetadata; @Nullable private Bitmap currentThumbnail; /*////////////////////////////////////////////////////////////////////////// // Player //////////////////////////////////////////////////////////////////////////*/ - private SimpleExoPlayer simpleExoPlayer; + private ExoPlayer simpleExoPlayer; private AudioReactor audioReactor; private MediaSessionManager mediaSessionManager; @Nullable private SurfaceHolderCallback surfaceHolderCallback; - @NonNull private final CustomTrackSelector trackSelector; + @NonNull private final DefaultTrackSelector trackSelector; @NonNull private final LoadController loadController; @NonNull private final RenderersFactory renderFactory; @@ -415,7 +431,7 @@ public final class Player implements setupBroadcastReceiver(); - trackSelector = new CustomTrackSelector(context, PlayerHelper.getQualitySelector()); + trackSelector = new DefaultTrackSelector(context, PlayerHelper.getQualitySelector()); final PlayerDataSource dataSource = new PlayerDataSource(context, DownloaderImpl.USER_AGENT, new DefaultBandwidthMeter.Builder(context).build()); loadController = new LoadController(); @@ -498,7 +514,7 @@ public final class Player implements Log.d(TAG, "initPlayer() called with: playOnReady = [" + playOnReady + "]"); } - simpleExoPlayer = new SimpleExoPlayer.Builder(context, renderFactory) + simpleExoPlayer = new ExoPlayer.Builder(context, renderFactory) .setTrackSelector(trackSelector) .setLoadControl(loadController) .build(); @@ -1642,8 +1658,7 @@ public final class Player implements } public boolean getPlaybackSkipSilence() { - return !exoPlayerIsNull() && simpleExoPlayer.getAudioComponent() != null - && simpleExoPlayer.getAudioComponent().getSkipSilenceEnabled(); + return !exoPlayerIsNull() && simpleExoPlayer.getSkipSilenceEnabled(); } public PlaybackParameters getPlaybackParameters() { @@ -1669,9 +1684,7 @@ public final class Player implements savePlaybackParametersToPrefs(this, roundedSpeed, roundedPitch, skipSilence); simpleExoPlayer.setPlaybackParameters( new PlaybackParameters(roundedSpeed, roundedPitch)); - if (simpleExoPlayer.getAudioComponent() != null) { - simpleExoPlayer.getAudioComponent().setSkipSilenceEnabled(skipSilence); - } + simpleExoPlayer.setSkipSilenceEnabled(skipSilence); } //endregion @@ -1950,10 +1963,12 @@ public final class Player implements final boolean showNext = playQueue.getIndex() + 1 != playQueue.getStreams().size(); final boolean showQueue = playQueue.getStreams().size() > 1 && !popupPlayerSelected(); boolean showSegment = false; - if (currentMetadata != null) { - showSegment = !currentMetadata.getMetadata().getStreamSegments().isEmpty() - && !popupPlayerSelected(); - } + showSegment = /*only when stream has segment and playing in fullscreen player*/ + !popupPlayerSelected() + && !getCurrentStreamInfo() + .map(StreamInfo::getStreamSegments) + .map(List::isEmpty) + .orElse(/*no stream info=*/true); binding.playPreviousButton.setVisibility(showPrev ? View.VISIBLE : View.INVISIBLE); binding.playPreviousButton.setAlpha(showPrev ? 1.0f : 0.0f); @@ -1993,9 +2008,30 @@ public final class Player implements // Playback states //////////////////////////////////////////////////////////////////////////*/ //region Playback states + @Override + public void onPlayWhenReadyChanged(final boolean playWhenReady, final int reason) { + if (DEBUG) { + Log.d(TAG, "ExoPlayer - onPlayWhenReadyChanged() called with: " + + "playWhenReady = [" + playWhenReady + "], " + + "reason = [" + reason + "]"); + } + final int playbackState = simpleExoPlayer == null + ? com.google.android.exoplayer2.Player.STATE_IDLE + : simpleExoPlayer.getPlaybackState(); + updatePlaybackState(playWhenReady, playbackState); + } - @Override // exoplayer listener - public void onPlayerStateChanged(final boolean playWhenReady, final int playbackState) { + @Override + public void onPlaybackStateChanged(final int playbackState) { + if (DEBUG) { + Log.d(TAG, "ExoPlayer - onPlaybackStateChanged() called with: " + + "playbackState = [" + playbackState + "]"); + } + final boolean playWhenReady = simpleExoPlayer != null && simpleExoPlayer.getPlayWhenReady(); + updatePlaybackState(playWhenReady, playbackState); + } + + private void updatePlaybackState(final boolean playWhenReady, final int playbackState) { if (DEBUG) { Log.d(TAG, "ExoPlayer - onPlayerStateChanged() called with: " + "playWhenReady = [" + playWhenReady + "], " @@ -2004,7 +2040,7 @@ public final class Player implements if (currentState == STATE_PAUSED_SEEK) { if (DEBUG) { - Log.d(TAG, "ExoPlayer - onPlayerStateChanged() is currently blocked"); + Log.d(TAG, "updatePlaybackState() is currently blocked"); } return; } @@ -2019,8 +2055,6 @@ public final class Player implements } break; case com.google.android.exoplayer2.Player.STATE_READY: //3 - maybeUpdateCurrentMetadata(); - maybeCorrectSeekPosition(); if (!isPrepared) { isPrepared = true; onPrepared(playWhenReady); @@ -2037,18 +2071,11 @@ public final class Player implements @Override // exoplayer listener public void onIsLoadingChanged(final boolean isLoading) { - if (DEBUG) { - Log.d(TAG, "ExoPlayer - onLoadingChanged() called with: " - + "isLoading = [" + isLoading + "]"); - } - if (!isLoading && currentState == STATE_PAUSED && isProgressLoopRunning()) { stopProgressLoop(); } else if (isLoading && !isProgressLoopRunning()) { startProgressLoop(); } - - maybeUpdateCurrentMetadata(); } @Override // own playback listener @@ -2460,27 +2487,37 @@ public final class Player implements //////////////////////////////////////////////////////////////////////////*/ //region ExoPlayer listeners (that didn't fit in other categories) - @Override - public void onTimelineChanged(@NonNull final Timeline timeline, final int reason) { - if (DEBUG) { - Log.d(TAG, "ExoPlayer - onTimelineChanged() called with " - + "timeline size = [" + timeline.getWindowCount() + "], " - + "reason = [" + reason + "]"); - } - - maybeUpdateCurrentMetadata(); - // force recreate notification to ensure seek bar is shown when preparation finishes - NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, true); + public void onEvents(@NonNull final com.google.android.exoplayer2.Player player, + @NonNull final com.google.android.exoplayer2.Player.Events events) { + Listener.super.onEvents(player, events); + MediaItemTag.from(player.getCurrentMediaItem()).ifPresent(tag -> { + if (tag == currentMetadata) { + return; + } + currentMetadata = tag; + if (!tag.getErrors().isEmpty()) { + final ErrorInfo errorInfo = new ErrorInfo( + tag.getErrors().get(0), + UserAction.PLAY_STREAM, + "Loading failed for [" + tag.getTitle() + "]: " + tag.getStreamUrl(), + tag.getServiceId()); + ErrorUtil.createNotification(context, errorInfo); + } + tag.getMaybeStreamInfo().ifPresent(info -> { + if (DEBUG) { + Log.d(TAG, "ExoPlayer - onEvents() update stream info: " + info.getName()); + } + updateMetadataWith(info); + }); + }); } @Override - public void onTracksChanged(@NonNull final TrackGroupArray trackGroups, - @NonNull final TrackSelectionArray trackSelections) { + public void onTracksInfoChanged(@NonNull final TracksInfo tracksInfo) { if (DEBUG) { Log.d(TAG, "ExoPlayer - onTracksChanged(), " - + "track group size = " + trackGroups.length); + + "track group size = " + tracksInfo.getTrackGroupInfos().size()); } - maybeUpdateCurrentMetadata(); onTextTracksChanged(); } @@ -2499,20 +2536,32 @@ public final class Player implements @DiscontinuityReason final int discontinuityReason) { if (DEBUG) { Log.d(TAG, "ExoPlayer - onPositionDiscontinuity() called with " + + "oldPositionIndex = [" + oldPosition.mediaItemIndex + "], " + + "oldPositionMs = [" + oldPosition.positionMs + "], " + + "newPositionIndex = [" + newPosition.mediaItemIndex + "], " + + "newPositionMs = [" + newPosition.positionMs + "], " + "discontinuityReason = [" + discontinuityReason + "]"); } if (playQueue == null) { return; } + if (newPosition.contentPositionMs == 0 && + simpleExoPlayer.getTotalBufferedDuration() < 500L) { + Log.d(TAG, "Playback - skipping to initial keyframe."); + simpleExoPlayer.setSeekParameters(SeekParameters.CLOSEST_SYNC); + simpleExoPlayer.seekTo(1L); + simpleExoPlayer.setSeekParameters(PlayerHelper.getSeekParameters(context)); + } + // Refresh the playback if there is a transition to the next video - final int newWindowIndex = simpleExoPlayer.getCurrentWindowIndex(); + final int newIndex = newPosition.mediaItemIndex; switch (discontinuityReason) { case DISCONTINUITY_REASON_AUTO_TRANSITION: case DISCONTINUITY_REASON_REMOVE: // When player is in single repeat mode and a period transition occurs, // we need to register a view count here since no metadata has changed - if (getRepeatMode() == REPEAT_MODE_ONE && newWindowIndex == playQueue.getIndex()) { + if (getRepeatMode() == REPEAT_MODE_ONE && newIndex == playQueue.getIndex()) { registerStreamViewed(); break; } @@ -2525,16 +2574,15 @@ public final class Player implements } case DISCONTINUITY_REASON_SEEK_ADJUSTMENT: case DISCONTINUITY_REASON_INTERNAL: - if (playQueue.getIndex() != newWindowIndex) { + // Player index may be invalid when playback is blocked + if (getCurrentState() != STATE_BLOCKED && newIndex != playQueue.getIndex()) { saveStreamProgressStateCompleted(); // current stream has ended - playQueue.setIndex(newWindowIndex); + playQueue.setIndex(newIndex); } break; case DISCONTINUITY_REASON_SKIP: break; // only makes Android Studio linter happy, as there are no ads } - - maybeUpdateCurrentMetadata(); } @Override @@ -2557,96 +2605,83 @@ public final class Player implements //region Errors /** * Process exceptions produced by {@link com.google.android.exoplayer2.ExoPlayer ExoPlayer}. - *

There are multiple types of errors:

- * * - * @see #processSourceError(IOException) - * @see com.google.android.exoplayer2.Player.Listener#onPlayerError(ExoPlaybackException) - */ + * @see com.google.android.exoplayer2.Player.Listener#onPlayerError(PlaybackException) + * */ + @SuppressLint("SwitchIntDef") @Override - public void onPlayerError(@NonNull final ExoPlaybackException error) { + public void onPlayerError(@NonNull final PlaybackException error) { Log.e(TAG, "ExoPlayer - onPlayerError() called with:", error); + setRecovery(); saveStreamProgressState(); boolean isCatchableException = false; - switch (error.type) { - case ExoPlaybackException.TYPE_SOURCE: - isCatchableException = processSourceError(error.getSourceException()); + switch (error.errorCode) { + case ERROR_CODE_BEHIND_LIVE_WINDOW: + isCatchableException = true; + simpleExoPlayer.seekToDefaultPosition(); + simpleExoPlayer.prepare(); + // Inform the user that we are reloading the stream by + // switching to the buffering state + onBuffering(); break; - case ExoPlaybackException.TYPE_UNEXPECTED: + case ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE: + case ERROR_CODE_IO_BAD_HTTP_STATUS: + case ERROR_CODE_IO_FILE_NOT_FOUND: + case ERROR_CODE_IO_NO_PERMISSION: + case ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED: + case ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE: + case ERROR_CODE_PARSING_CONTAINER_MALFORMED: + case ERROR_CODE_PARSING_MANIFEST_MALFORMED: + case ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED: + case ERROR_CODE_PARSING_MANIFEST_UNSUPPORTED: + // Source errors, signal on playQueue and move on: + if (!exoPlayerIsNull() && playQueue != null) { + isCatchableException = true; + playQueue.error(); + } + break; + case ERROR_CODE_TIMEOUT: + case ERROR_CODE_IO_UNSPECIFIED: + case ERROR_CODE_IO_NETWORK_CONNECTION_FAILED: + case ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT: + // Don't create notification on timeout/networking errors: + isCatchableException = true; + case ERROR_CODE_UNSPECIFIED: + // Reload playback on unexpected errors: setRecovery(); reloadPlayQueueManager(); break; - case ExoPlaybackException.TYPE_REMOTE: - case ExoPlaybackException.TYPE_RENDERER: default: + // API, remote and renderer errors belong here: onPlaybackShutdown(); break; } - if (isCatchableException) { - return; + if (!isCatchableException) { + createErrorNotification(error); } - createErrorNotification(error); - if (fragmentListener != null) { - fragmentListener.onPlayerError(error); + fragmentListener.onPlayerError(error, isCatchableException); } } - private void createErrorNotification(@NonNull final ExoPlaybackException error) { + private void createErrorNotification(@NonNull final PlaybackException error) { final ErrorInfo errorInfo; if (currentMetadata == null) { errorInfo = new ErrorInfo(error, UserAction.PLAY_STREAM, - "Player error[type=" + error.type + "] occurred, currentMetadata is null"); + "Player error[type=" + error.getErrorCodeName() + + "] occurred, currentMetadata is null"); } else { errorInfo = new ErrorInfo(error, UserAction.PLAY_STREAM, - "Player error[type=" + error.type + "] occurred while playing " - + currentMetadata.getMetadata().getUrl(), - currentMetadata.getMetadata()); + "Player error[type=" + error.getErrorCodeName() + + "] occurred while playing " + currentMetadata.getStreamUrl(), + currentMetadata.getServiceId()); } ErrorUtil.createNotification(context, errorInfo); } - - /** - * Process an {@link IOException} returned by {@link ExoPlaybackException#getSourceException()} - * for {@link ExoPlaybackException#TYPE_SOURCE} exceptions. - * - *

- * This method sets the recovery position and sends an error message to the play queue if the - * exception is not a {@link BehindLiveWindowException}. - *

- * @param error the source error which was thrown by ExoPlayer - * @return whether the exception thrown is a {@link BehindLiveWindowException} ({@code false} - * is always returned if ExoPlayer or the play queue is null) - */ - private boolean processSourceError(final IOException error) { - if (exoPlayerIsNull() || playQueue == null) { - return false; - } - - setRecovery(); - - if (error instanceof BehindLiveWindowException) { - simpleExoPlayer.seekToDefaultPosition(); - simpleExoPlayer.prepare(); - // Inform the user that we are reloading the stream by switching to the buffering state - onBuffering(); - return true; - } - - playQueue.error(); - return false; - } //endregion @@ -2693,7 +2728,7 @@ public final class Player implements } final Timeline currentTimeline = simpleExoPlayer.getCurrentTimeline(); - final int currentWindowIndex = simpleExoPlayer.getCurrentWindowIndex(); + final int currentWindowIndex = simpleExoPlayer.getCurrentMediaItemIndex(); if (currentTimeline.isEmpty() || currentWindowIndex < 0 || currentWindowIndex >= currentTimeline.getWindowCount()) { return false; @@ -2705,10 +2740,10 @@ public final class Player implements } @Override // own playback listener - public void onPlaybackSynchronize(@NonNull final PlayQueueItem item) { + public void onPlaybackSynchronize(@NonNull final PlayQueueItem item, final boolean wasBlocked) { if (DEBUG) { - Log.d(TAG, "Playback - onPlaybackSynchronize() called with " - + "item=[" + item.getTitle() + "], url=[" + item.getUrl() + "]"); + Log.d(TAG, "Playback - onPlaybackSynchronize(was blocked: " + wasBlocked + + ") called with item=[" + item.getTitle() + "], url=[" + item.getUrl() + "]"); } if (exoPlayerIsNull() || playQueue == null) { return; @@ -2718,7 +2753,7 @@ public final class Player implements final boolean hasPlayQueueItemChanged = currentItem != item; final int currentPlayQueueIndex = playQueue.indexOf(item); - final int currentPlaylistIndex = simpleExoPlayer.getCurrentWindowIndex(); + final int currentPlaylistIndex = simpleExoPlayer.getCurrentMediaItemIndex(); final int currentPlaylistSize = simpleExoPlayer.getCurrentTimeline().getWindowCount(); // If nothing to synchronize @@ -2740,8 +2775,7 @@ public final class Player implements + "index=[" + currentPlayQueueIndex + "] with " + "playlist length=[" + currentPlaylistSize + "]"); - } else if (currentPlaylistIndex != currentPlayQueueIndex || onPlaybackInitial - || !isPlaying()) { + } else if (wasBlocked || currentPlaylistIndex != currentPlayQueueIndex) { if (DEBUG) { Log.d(TAG, "Playback - Rewinding to correct " + "index=[" + currentPlayQueueIndex + "], " @@ -2758,28 +2792,6 @@ public final class Player implements } } - private void maybeCorrectSeekPosition() { - if (playQueue == null || exoPlayerIsNull() || currentMetadata == null) { - return; - } - - final PlayQueueItem currentSourceItem = playQueue.getItem(); - if (currentSourceItem == null) { - return; - } - - final StreamInfo currentInfo = currentMetadata.getMetadata(); - final long presetStartPositionMillis = currentInfo.getStartPosition() * 1000; - if (presetStartPositionMillis > 0L) { - // Has another start position? - if (DEBUG) { - Log.d(TAG, "Playback - Seeking to preset start " - + "position=[" + presetStartPositionMillis + "]"); - } - seekTo(presetStartPositionMillis); - } - } - public void seekTo(final long positionMillis) { if (DEBUG) { Log.d(TAG, "seekBy() called with: position = [" + positionMillis + "]"); @@ -2941,24 +2953,24 @@ public final class Player implements //region StreamInfo history: views and progress private void registerStreamViewed() { - if (currentMetadata != null) { - databaseUpdateDisposable.add(recordManager.onViewed(currentMetadata.getMetadata()) - .onErrorComplete().subscribe()); - } + getCurrentStreamInfo().ifPresent(info -> { + databaseUpdateDisposable + .add(recordManager.onViewed(info).onErrorComplete().subscribe()); + }); } private void saveStreamProgressState(final long progressMillis) { - if (currentMetadata == null + if (!getCurrentStreamInfo().isPresent() || !prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)) { return; } if (DEBUG) { Log.d(TAG, "saveStreamProgressState() called with: progressMillis=" + progressMillis - + ", currentMetadata=[" + currentMetadata.getMetadata().getName() + "]"); + + ", currentMetadata=[" + getCurrentStreamInfo().get().getName() + "]"); } databaseUpdateDisposable - .add(recordManager.saveStreamState(currentMetadata.getMetadata(), progressMillis) + .add(recordManager.saveStreamState(getCurrentStreamInfo().get(), progressMillis) .observeOn(AndroidSchedulers.mainThread()) .doOnError(e -> { if (DEBUG) { @@ -2971,7 +2983,7 @@ public final class Player implements public void saveStreamProgressState() { if (exoPlayerIsNull() || currentMetadata == null || playQueue == null - || playQueue.getIndex() != simpleExoPlayer.getCurrentWindowIndex()) { + || playQueue.getIndex() != simpleExoPlayer.getCurrentMediaItemIndex()) { // Make sure play queue and current window index are equal, to prevent saving state for // the wrong stream on discontinuity (e.g. when the stream just changed but the // playQueue index and currentMetadata still haven't updated) @@ -2984,10 +2996,10 @@ public final class Player implements } public void saveStreamProgressStateCompleted() { - if (currentMetadata != null) { + getCurrentStreamInfo().ifPresent(info -> { // current stream has ended, so the progress is its duration (+1 to overcome rounding) - saveStreamProgressState((currentMetadata.getMetadata().getDuration() + 1) * 1000); - } + saveStreamProgressState((info.getDuration() + 1) * 1000); + }); } //endregion @@ -2998,8 +3010,7 @@ public final class Player implements //////////////////////////////////////////////////////////////////////////*/ //region Metadata - private void onMetadataChanged(@NonNull final MediaSourceTag tag) { - final StreamInfo info = tag.getMetadata(); + private void onMetadataChanged(@NonNull final StreamInfo info) { if (DEBUG) { Log.d(TAG, "Playback - onMetadataChanged() called, playing: " + info.getName()); } @@ -3009,12 +3020,10 @@ public final class Player implements updateStreamRelatedViews(); showHideKodiButton(); - binding.titleTextView.setText(tag.getMetadata().getName()); - binding.channelTextView.setText(tag.getMetadata().getUploaderName()); + binding.titleTextView.setText(info.getName()); + binding.channelTextView.setText(info.getUploaderName()); - this.seekbarPreviewThumbnailHolder.resetFrom( - this.getContext(), - tag.getMetadata().getPreviewFrames()); + this.seekbarPreviewThumbnailHolder.resetFrom(this.getContext(), info.getPreviewFrames()); NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); @@ -3024,9 +3033,7 @@ public final class Player implements getVideoTitle(), getUploaderName(), showThumbnail ? Optional.ofNullable(getThumbnail()) : Optional.empty(), - StreamTypeUtil.isLiveStream(tag.getMetadata().getStreamType()) - ? -1 - : tag.getMetadata().getDuration() + StreamTypeUtil.isLiveStream(info.getStreamType()) ? -1 : info.getDuration() ); notifyMetadataUpdateToListeners(); @@ -3043,40 +3050,21 @@ public final class Player implements } } - private void maybeUpdateCurrentMetadata() { + private void updateMetadataWith(@NonNull final StreamInfo streamInfo) { if (exoPlayerIsNull()) { return; } - final MediaSourceTag metadata; - try { - final MediaItem currentMediaItem = simpleExoPlayer.getCurrentMediaItem(); - if (currentMediaItem == null || currentMediaItem.playbackProperties == null - || currentMediaItem.playbackProperties.tag == null) { - return; - } - metadata = (MediaSourceTag) currentMediaItem.playbackProperties.tag; - } catch (final IndexOutOfBoundsException | ClassCastException ex) { - if (DEBUG) { - Log.d(TAG, "Could not update metadata", ex); - } - return; - } - - maybeAutoQueueNextStream(metadata); - - if (currentMetadata == metadata) { - return; - } - currentMetadata = metadata; - onMetadataChanged(metadata); + maybeAutoQueueNextStream(streamInfo); + onMetadataChanged(streamInfo); + NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, true); } @NonNull private String getVideoUrl() { return currentMetadata == null ? context.getString(R.string.unknown_content) - : currentMetadata.getMetadata().getUrl(); + : currentMetadata.getStreamUrl(); } @NonNull @@ -3084,7 +3072,7 @@ public final class Player implements final int timeSeconds = binding.playbackSeekBar.getProgress() / 1000; String videoUrl = getVideoUrl(); if (!isLive() && timeSeconds >= 0 && currentMetadata != null - && currentMetadata.getMetadata().getServiceId() == YouTube.getServiceId()) { + && currentMetadata.getServiceId() == YouTube.getServiceId()) { // Timestamp doesn't make sense in a live stream so drop it videoUrl += ("&t=" + timeSeconds); } @@ -3095,14 +3083,14 @@ public final class Player implements public String getVideoTitle() { return currentMetadata == null ? context.getString(R.string.unknown_content) - : currentMetadata.getMetadata().getName(); + : currentMetadata.getTitle(); } @NonNull public String getUploaderName() { return currentMetadata == null ? context.getString(R.string.unknown_content) - : currentMetadata.getMetadata().getUploaderName(); + : currentMetadata.getUploaderName(); } @Nullable @@ -3122,14 +3110,14 @@ public final class Player implements //////////////////////////////////////////////////////////////////////////*/ //region Play queue, segments and streams - private void maybeAutoQueueNextStream(@NonNull final MediaSourceTag metadata) { + private void maybeAutoQueueNextStream(@NonNull final StreamInfo info) { if (playQueue == null || playQueue.getIndex() != playQueue.size() - 1 || getRepeatMode() != REPEAT_MODE_OFF || !PlayerHelper.isAutoQueueEnabled(context)) { return; } // auto queue when starting playback on the last item when not repeating - final PlayQueue autoQueue = PlayerHelper.autoQueueOf(metadata.getMetadata(), + final PlayQueue autoQueue = PlayerHelper.autoQueueOf(info, playQueue.getStreams()); if (autoQueue != null) { playQueue.append(autoQueue.getStreams()); @@ -3232,9 +3220,7 @@ public final class Player implements itemTouchHelper.attachToRecyclerView(null); } - if (currentMetadata != null) { - segmentAdapter.setItems(currentMetadata.getMetadata()); - } + getCurrentStreamInfo().ifPresent(segmentAdapter::setItems); binding.shuffleButton.setVisibility(View.GONE); binding.repeatButton.setVisibility(View.GONE); @@ -3288,7 +3274,9 @@ public final class Player implements private int getNearestStreamSegmentPosition(final long playbackPosition) { int nearestPosition = 0; - final List segments = currentMetadata.getMetadata().getStreamSegments(); + final List segments = getCurrentStreamInfo() + .map(StreamInfo::getStreamSegments) + .orElse(Collections.emptyList()); for (int i = 0; i < segments.size(); i++) { if (segments.get(i).getStartTimeSeconds() * 1000L > playbackPosition) { @@ -3379,10 +3367,10 @@ public final class Player implements } private void updateStreamRelatedViews() { - if (currentMetadata == null) { + if (!getCurrentStreamInfo().isPresent()) { return; } - final StreamInfo info = currentMetadata.getMetadata(); + final StreamInfo info = getCurrentStreamInfo().get(); binding.qualityTextView.setVisibility(View.GONE); binding.playbackSpeed.setVisibility(View.GONE); @@ -3410,12 +3398,15 @@ public final class Player implements break; case VIDEO_STREAM: - if (info.getVideoStreams().size() + info.getVideoOnlyStreams().size() == 0) { + if (currentMetadata == null + || !currentMetadata.getMaybeQuality().isPresent() + || info.getVideoStreams().size() + info.getVideoOnlyStreams().size() == 0) { break; } - availableStreams = currentMetadata.getSortedAvailableVideoStreams(); - selectedStreamIndex = currentMetadata.getSelectedVideoStreamIndex(); + availableStreams = currentMetadata.getMaybeQuality().get().getSortedVideoStreams(); + selectedStreamIndex = + currentMetadata.getMaybeQuality().get().getSelectedVideoStreamIndex(); buildQualityMenu(); binding.qualityTextView.setVisibility(View.VISIBLE); @@ -3535,8 +3526,8 @@ public final class Player implements captionItem.setOnMenuItemClickListener(menuItem -> { final int textRendererIndex = getCaptionRendererIndex(); if (textRendererIndex != RENDERER_UNAVAILABLE) { - trackSelector.setPreferredTextLanguage(captionLanguage); trackSelector.setParameters(trackSelector.buildUponParameters() + .setPreferredTextLanguage(captionLanguage) .setRendererDisabled(textRendererIndex, false)); prefs.edit().putString(context.getString(R.string.caption_user_set_key), captionLanguage).apply(); @@ -3551,8 +3542,8 @@ public final class Player implements userPreferredLanguage.substring(0, userPreferredLanguage.indexOf('(')))))) { final int textRendererIndex = getCaptionRendererIndex(); if (textRendererIndex != RENDERER_UNAVAILABLE) { - trackSelector.setPreferredTextLanguage(captionLanguage); trackSelector.setParameters(trackSelector.buildUponParameters() + .setPreferredTextLanguage(captionLanguage) .setRendererDisabled(textRendererIndex, false)); } searchForAutogenerated = false; @@ -3679,7 +3670,10 @@ public final class Player implements } // Normalize mismatching language strings - final String preferredLanguage = trackSelector.getPreferredTextLanguage(); + final List preferredLanguages = + trackSelector.getParameters().preferredTextLanguages; + final String preferredLanguage = + preferredLanguages.isEmpty() ? null : preferredLanguages.get(0); // Build UI buildCaptionMenu(availableLanguages); if (trackSelector.getParameters().getRendererDisabled(textRenderer) @@ -3886,10 +3880,9 @@ public final class Player implements } private void onOpenInBrowserClicked() { - if (currentMetadata != null) { - ShareUtils.openUrlInBrowser(getParentActivity(), - currentMetadata.getMetadata().getOriginalUrl()); - } + getCurrentStreamInfo().map(Info::getOriginalUrl).ifPresent(originalUrl -> { + ShareUtils.openUrlInBrowser(Objects.requireNonNull(getParentActivity()), originalUrl); + }); } //endregion @@ -4145,12 +4138,14 @@ public final class Player implements } private void notifyMetadataUpdateToListeners() { - if (fragmentListener != null && currentMetadata != null) { - fragmentListener.onMetadataUpdate(currentMetadata.getMetadata(), playQueue); - } - if (activityListener != null && currentMetadata != null) { - activityListener.onMetadataUpdate(currentMetadata.getMetadata(), playQueue); - } + getCurrentStreamInfo().ifPresent(info -> { + if (fragmentListener != null) { + fragmentListener.onMetadataUpdate(info, playQueue); + } + if (activityListener != null) { + activityListener.onMetadataUpdate(info, playQueue); + } + }); } private void notifyPlaybackUpdateToListeners() { @@ -4201,14 +4196,14 @@ public final class Player implements // in livestreams) so we will be not able to execute the block below. // Reload the play queue manager in this case, which is the behavior when we don't know the // index of the video renderer or playQueueManagerReloadingNeeded returns true. - if (currentMetadata == null) { + if (!getCurrentStreamInfo().isPresent()) { reloadPlayQueueManager(); setRecovery(); return; } final int videoRenderIndex = getVideoRendererIndex(); - final StreamInfo info = currentMetadata.getMetadata(); + final StreamInfo info = getCurrentStreamInfo().get(); // In the case we don't know the source type, fallback to the one with video with audio or // audio-only source. @@ -4313,6 +4308,10 @@ public final class Player implements //////////////////////////////////////////////////////////////////////////*/ //region Getters + private Optional getCurrentStreamInfo() { + return Optional.ofNullable(currentMetadata).flatMap(MediaItemTag::getMaybeStreamInfo); + } + public int getCurrentState() { return currentState; } @@ -4322,8 +4321,7 @@ public final class Player implements } public boolean isStopped() { - return exoPlayerIsNull() - || simpleExoPlayer.getPlaybackState() == SimpleExoPlayer.STATE_IDLE; + return exoPlayerIsNull() || simpleExoPlayer.getPlaybackState() == ExoPlayer.STATE_IDLE; } public boolean isPlaying() { @@ -4340,7 +4338,7 @@ public final class Player implements private boolean isLive() { try { - return !exoPlayerIsNull() && simpleExoPlayer.isCurrentWindowDynamic(); + return !exoPlayerIsNull() && simpleExoPlayer.isCurrentMediaItemDynamic(); } catch (final IndexOutOfBoundsException e) { // Why would this even happen =(... but lets log it anyway, better safe than sorry if (DEBUG) { @@ -4519,9 +4517,13 @@ public final class Player implements surfaceHolderCallback = new SurfaceHolderCallback(context, simpleExoPlayer); binding.surfaceView.getHolder().addCallback(surfaceHolderCallback); final Surface surface = binding.surfaceView.getHolder().getSurface(); - // initially set the surface manually otherwise - // onRenderedFirstFrame() will not be called - simpleExoPlayer.setVideoSurface(surface); + // ensure player is using an unreleased surface, which the surfaceView might not be + // when starting playback on background or during player switching + if (surface.isValid()) { + // initially set the surface manually otherwise + // onRenderedFirstFrame() will not be called + simpleExoPlayer.setVideoSurface(surface); + } } else { simpleExoPlayer.setVideoSurfaceView(binding.surfaceView); } diff --git a/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceEventListener.java b/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceEventListener.java index f8d03087e..359eab8b2 100644 --- a/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceEventListener.java +++ b/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceEventListener.java @@ -1,6 +1,6 @@ package org.schabi.newpipe.player.event; -import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.PlaybackException; public interface PlayerServiceEventListener extends PlayerEventListener { void onFullscreenStateChanged(boolean fullscreen); @@ -9,7 +9,7 @@ public interface PlayerServiceEventListener extends PlayerEventListener { void onMoreOptionsLongClicked(); - void onPlayerError(ExoPlaybackException error); + void onPlayerError(PlaybackException error, boolean isCatchableException); void hideSystemUiIfNeeded(); } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java index 087a3bc76..a05990816 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java @@ -14,7 +14,7 @@ import androidx.core.content.ContextCompat; import androidx.media.AudioFocusRequestCompat; import androidx.media.AudioManagerCompat; -import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.analytics.AnalyticsListener; public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, AnalyticsListener { @@ -27,14 +27,14 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, An private static final int FOCUS_GAIN_TYPE = AudioManagerCompat.AUDIOFOCUS_GAIN; private static final int STREAM_TYPE = AudioManager.STREAM_MUSIC; - private final SimpleExoPlayer player; + private final ExoPlayer player; private final Context context; private final AudioManager audioManager; private final AudioFocusRequestCompat request; public AudioReactor(@NonNull final Context context, - @NonNull final SimpleExoPlayer player) { + @NonNull final ExoPlayer player) { this.player = player; this.context = context; this.audioManager = ContextCompat.getSystemService(context, AudioManager.class); diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java b/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java index bcab92787..98e04d466 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java @@ -3,12 +3,10 @@ package org.schabi.newpipe.player.helper; import android.content.Context; import android.util.Log; -import androidx.annotation.NonNull; - -import com.google.android.exoplayer2.database.ExoDatabaseProvider; +import com.google.android.exoplayer2.database.StandaloneDatabaseProvider; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultDataSource; -import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; import com.google.android.exoplayer2.upstream.FileDataSource; import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.upstream.cache.CacheDataSink; @@ -18,6 +16,8 @@ import com.google.android.exoplayer2.upstream.cache.SimpleCache; import java.io.File; +import androidx.annotation.NonNull; + /* package-private */ class CacheFactory implements DataSource.Factory { private static final String TAG = "CacheFactory"; @@ -25,7 +25,7 @@ import java.io.File; private static final int CACHE_FLAGS = CacheDataSource.FLAG_BLOCK_ON_CACHE | CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR; - private final DefaultDataSourceFactory dataSourceFactory; + private final DataSource.Factory dataSourceFactory; private final File cacheDir; private final long maxFileSize; @@ -49,7 +49,9 @@ import java.io.File; final long maxFileSize) { this.maxFileSize = maxFileSize; - dataSourceFactory = new DefaultDataSourceFactory(context, userAgent, transferListener); + dataSourceFactory = new DefaultDataSource + .Factory(context, new DefaultHttpDataSource.Factory().setUserAgent(userAgent)) + .setTransferListener(transferListener); cacheDir = new File(context.getExternalCacheDir(), CACHE_FOLDER_NAME); if (!cacheDir.exists()) { //noinspection ResultOfMethodCallIgnored @@ -59,7 +61,7 @@ import java.io.File; if (cache == null) { final LeastRecentlyUsedCacheEvictor evictor = new LeastRecentlyUsedCacheEvictor(maxCacheSize); - cache = new SimpleCache(cacheDir, evictor, new ExoDatabaseProvider(context)); + cache = new SimpleCache(cacheDir, evictor, new StandaloneDatabaseProvider(context)); } } @@ -68,7 +70,7 @@ import java.io.File; public DataSource createDataSource() { Log.d(TAG, "initExoPlayerCache: cacheDir = " + cacheDir.getAbsolutePath()); - final DefaultDataSource dataSource = dataSourceFactory.createDataSource(); + final DataSource dataSource = dataSourceFactory.createDataSource(); final FileDataSource fileSource = new FileDataSource(); final CacheDataSink dataSink = new CacheDataSink(cache, maxFileSize); diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java b/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java index cd04bc2eb..c12ba754a 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java @@ -19,7 +19,6 @@ import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector; import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.player.mediasession.MediaSessionCallback; import org.schabi.newpipe.player.mediasession.PlayQueueNavigator; -import org.schabi.newpipe.player.mediasession.PlayQueuePlaybackController; import java.util.Optional; @@ -55,7 +54,6 @@ public class MediaSessionManager { .build()); sessionConnector = new MediaSessionConnector(mediaSession); - sessionConnector.setControlDispatcher(new PlayQueuePlaybackController(callback)); sessionConnector.setQueueNavigator(new PlayQueueNavigator(mediaSession, callback)); sessionConnector.setPlayer(player); } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java index d7a9ffc3d..405f6fd37 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java @@ -2,8 +2,6 @@ package org.schabi.newpipe.player.helper; import android.content.Context; -import androidx.annotation.NonNull; - import com.google.android.exoplayer2.source.ProgressiveMediaSource; import com.google.android.exoplayer2.source.SingleSampleMediaSource; import com.google.android.exoplayer2.source.dash.DashMediaSource; @@ -13,10 +11,13 @@ import com.google.android.exoplayer2.source.hls.playlist.DefaultHlsPlaylistTrack import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource; import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import com.google.android.exoplayer2.upstream.DefaultDataSource; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.TransferListener; +import androidx.annotation.NonNull; + public class PlayerDataSource { public static final int LIVE_STREAM_EDGE_GAP_MILLIS = 10000; @@ -35,12 +36,14 @@ public class PlayerDataSource { private final DataSource.Factory cacheDataSourceFactory; private final DataSource.Factory cachelessDataSourceFactory; - public PlayerDataSource(@NonNull final Context context, @NonNull final String userAgent, + public PlayerDataSource(@NonNull final Context context, + @NonNull final String userAgent, @NonNull final TransferListener transferListener) { continueLoadingCheckIntervalBytes = PlayerHelper.getProgressiveLoadIntervalBytes(context); cacheDataSourceFactory = new CacheFactory(context, userAgent, transferListener); - cachelessDataSourceFactory - = new DefaultDataSourceFactory(context, userAgent, transferListener); + cachelessDataSourceFactory = new DefaultDataSource + .Factory(context, new DefaultHttpDataSource.Factory().setUserAgent(userAgent)) + .setTransferListener(transferListener); } public SsMediaSource.Factory getLiveSsMediaSourceFactory() { diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java index 06a2e52ab..4c09ed3c1 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java @@ -10,7 +10,7 @@ import android.util.Log; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; -import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.PlaybackException; import com.google.android.exoplayer2.PlaybackParameters; import org.schabi.newpipe.App; @@ -233,9 +233,10 @@ public final class PlayerHolder { } @Override - public void onPlayerError(final ExoPlaybackException error) { + public void onPlayerError(final PlaybackException error, + final boolean isCatchableException) { if (listener != null) { - listener.onPlayerError(error); + listener.onPlayerError(error, isCatchableException); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediaitem/ExceptionTag.java b/app/src/main/java/org/schabi/newpipe/player/mediaitem/ExceptionTag.java new file mode 100644 index 000000000..2deffcf65 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/mediaitem/ExceptionTag.java @@ -0,0 +1,99 @@ +package org.schabi.newpipe.player.mediaitem; + +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.extractor.stream.StreamType; +import org.schabi.newpipe.player.playqueue.PlayQueueItem; + +import java.util.List; +import java.util.Optional; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public final class ExceptionTag implements MediaItemTag { + @NonNull + private final PlayQueueItem item; + @NonNull + private final List errors; + @Nullable + private final Object extras; + + private ExceptionTag(@NonNull final PlayQueueItem item, + @NonNull final List errors, + @Nullable final Object extras) { + this.item = item; + this.errors = errors; + this.extras = extras; + } + + public static ExceptionTag of(@NonNull final PlayQueueItem playQueueItem, + @NonNull final List errors) { + return new ExceptionTag(playQueueItem, errors, null); + } + + @NonNull + @Override + public List getErrors() { + return errors; + } + + @Override + public int getServiceId() { + return item.getServiceId(); + } + + @Override + public String getTitle() { + return item.getTitle(); + } + + @Override + public String getUploaderName() { + return item.getUploader(); + } + + @Override + public long getDurationSeconds() { + return item.getDuration(); + } + + @Override + public String getStreamUrl() { + return item.getUrl(); + } + + @Override + public String getThumbnailUrl() { + return item.getThumbnailUrl(); + } + + @Override + public String getUploaderUrl() { + return item.getUploaderUrl(); + } + + @Override + public StreamType getStreamType() { + return item.getStreamType(); + } + + @Override + public Optional getMaybeStreamInfo() { + return Optional.empty(); + } + + @Override + public Optional getMaybeQuality() { + return Optional.empty(); + } + + @Override + public Optional getMaybeExtras(@NonNull final Class type) { + return Optional.ofNullable(extras).map(type::cast); + } + + @Override + public MediaItemTag withExtras(@NonNull final T extra) { + return new ExceptionTag(item, errors, extra); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/mediaitem/MediaItemTag.java b/app/src/main/java/org/schabi/newpipe/player/mediaitem/MediaItemTag.java new file mode 100644 index 000000000..872a10a57 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/mediaitem/MediaItemTag.java @@ -0,0 +1,113 @@ +package org.schabi.newpipe.player.mediaitem; + +import android.net.Uri; + +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.MediaMetadata; + +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.extractor.stream.StreamType; +import org.schabi.newpipe.extractor.stream.VideoStream; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public interface MediaItemTag { + + List getErrors(); + + int getServiceId(); + + String getTitle(); + + String getUploaderName(); + + long getDurationSeconds(); + + String getStreamUrl(); + + String getThumbnailUrl(); + + String getUploaderUrl(); + + StreamType getStreamType(); + + Optional getMaybeStreamInfo(); + + Optional getMaybeQuality(); + + Optional getMaybeExtras(@NonNull Class type); + + MediaItemTag withExtras(@NonNull T extra); + + @NonNull + static Optional from(@Nullable final MediaItem mediaItem) { + if (mediaItem == null || mediaItem.localConfiguration == null + || !(mediaItem.localConfiguration.tag instanceof MediaItemTag)) { + return Optional.empty(); + } + + return Optional.of((MediaItemTag) mediaItem.localConfiguration.tag); + } + + @NonNull + default String makeMediaId() { + return UUID.randomUUID().toString() + "[" + getTitle() + "]"; + } + + @NonNull + default MediaItem asMediaItem() { + final MediaMetadata mediaMetadata = new MediaMetadata.Builder() + .setMediaUri(Uri.parse(getStreamUrl())) + .setArtworkUri(Uri.parse(getThumbnailUrl())) + .setArtist(getUploaderName()) + .setDescription(getTitle()) + .setDisplayTitle(getTitle()) + .setTitle(getTitle()) + .build(); + + return MediaItem.fromUri(getStreamUrl()) + .buildUpon() + .setMediaId(makeMediaId()) + .setMediaMetadata(mediaMetadata) + .setTag(this) + .build(); + } + + class Quality { + @NonNull + private final List sortedVideoStreams; + private final int selectedVideoStreamIndex; + + private Quality(@NonNull final List sortedVideoStreams, + final int selectedVideoStreamIndex) { + this.sortedVideoStreams = sortedVideoStreams; + this.selectedVideoStreamIndex = selectedVideoStreamIndex; + } + + static Quality of(@NonNull final List sortedVideoStreams, + final int selectedVideoStreamIndex) { + return new Quality(sortedVideoStreams, selectedVideoStreamIndex); + } + + @NonNull + public List getSortedVideoStreams() { + return sortedVideoStreams; + } + + public int getSelectedVideoStreamIndex() { + return selectedVideoStreamIndex; + } + + @Nullable + public VideoStream getSelectedVideoStream() { + return selectedVideoStreamIndex < 0 + || selectedVideoStreamIndex >= sortedVideoStreams.size() + ? null : sortedVideoStreams.get(selectedVideoStreamIndex); + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/mediaitem/PlaceholderTag.java b/app/src/main/java/org/schabi/newpipe/player/mediaitem/PlaceholderTag.java new file mode 100644 index 000000000..c4998e9af --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/mediaitem/PlaceholderTag.java @@ -0,0 +1,89 @@ +package org.schabi.newpipe.player.mediaitem; + +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.extractor.stream.StreamType; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public final class PlaceholderTag implements MediaItemTag { + public static final PlaceholderTag EMPTY = new PlaceholderTag(null); + private static final String UNKNOWN_VALUE_INTERNAL = "Placeholder"; + + @Nullable + private final Object extras; + + private PlaceholderTag(@Nullable final Object extras) { + this.extras = extras; + } + + @NonNull + @Override + public List getErrors() { + return Collections.emptyList(); + } + + @Override + public int getServiceId() { + return -1; + } + + @Override + public String getTitle() { + return UNKNOWN_VALUE_INTERNAL; + } + + @Override + public String getUploaderName() { + return UNKNOWN_VALUE_INTERNAL; + } + + @Override + public long getDurationSeconds() { + return -1; + } + + @Override + public String getStreamUrl() { + return UNKNOWN_VALUE_INTERNAL; + } + + @Override + public String getThumbnailUrl() { + return UNKNOWN_VALUE_INTERNAL; + } + + @Override + public String getUploaderUrl() { + return UNKNOWN_VALUE_INTERNAL; + } + + @Override + public StreamType getStreamType() { + return StreamType.NONE; + } + + @Override + public Optional getMaybeStreamInfo() { + return Optional.empty(); + } + + @Override + public Optional getMaybeQuality() { + return Optional.empty(); + } + + @Override + public Optional getMaybeExtras(@NonNull final Class type) { + return Optional.ofNullable(extras).map(type::cast); + } + + @Override + public MediaItemTag withExtras(@NonNull final T extra) { + return new PlaceholderTag(extra); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/mediaitem/StreamInfoTag.java b/app/src/main/java/org/schabi/newpipe/player/mediaitem/StreamInfoTag.java new file mode 100644 index 000000000..93cf081f9 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/mediaitem/StreamInfoTag.java @@ -0,0 +1,105 @@ +package org.schabi.newpipe.player.mediaitem; + +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.extractor.stream.StreamType; +import org.schabi.newpipe.extractor.stream.VideoStream; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public final class StreamInfoTag implements MediaItemTag { + @NonNull + private final StreamInfo streamInfo; + @Nullable + private final MediaItemTag.Quality quality; + @Nullable + private final Object extras; + + private StreamInfoTag(@NonNull final StreamInfo streamInfo, + @Nullable final MediaItemTag.Quality quality, + @Nullable final Object extras) { + this.streamInfo = streamInfo; + this.quality = quality; + this.extras = extras; + } + + public static StreamInfoTag of(@NonNull final StreamInfo streamInfo, + @NonNull final List sortedVideoStreams, + final int selectedVideoStreamIndex) { + final Quality quality = Quality.of(sortedVideoStreams, selectedVideoStreamIndex); + return new StreamInfoTag(streamInfo, quality, null); + } + + public static StreamInfoTag of(@NonNull final StreamInfo streamInfo) { + return new StreamInfoTag(streamInfo, null, null); + } + + @Override + public List getErrors() { + return Collections.emptyList(); + } + + @Override + public int getServiceId() { + return streamInfo.getServiceId(); + } + + @Override + public String getTitle() { + return streamInfo.getName(); + } + + @Override + public String getUploaderName() { + return streamInfo.getUploaderName(); + } + + @Override + public long getDurationSeconds() { + return streamInfo.getDuration(); + } + + @Override + public String getStreamUrl() { + return streamInfo.getUrl(); + } + + @Override + public String getThumbnailUrl() { + return streamInfo.getThumbnailUrl(); + } + + @Override + public String getUploaderUrl() { + return streamInfo.getUploaderUrl(); + } + + @Override + public StreamType getStreamType() { + return streamInfo.getStreamType(); + } + + @Override + public Optional getMaybeStreamInfo() { + return Optional.of(streamInfo); + } + + @Override + public Optional getMaybeQuality() { + return Optional.ofNullable(quality); + } + + @Override + public Optional getMaybeExtras(@NonNull final Class type) { + return Optional.ofNullable(extras).map(type::cast); + } + + @Override + public StreamInfoTag withExtras(@NonNull final Object extra) { + return new StreamInfoTag(streamInfo, quality, extra); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueueNavigator.java b/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueueNavigator.java index 62664c827..92cd425c5 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueueNavigator.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueueNavigator.java @@ -7,7 +7,6 @@ import android.support.v4.media.session.MediaSessionCompat; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.google.android.exoplayer2.ControlDispatcher; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector; import com.google.android.exoplayer2.util.Util; @@ -44,17 +43,17 @@ public class PlayQueueNavigator implements MediaSessionConnector.QueueNavigator } @Override - public void onTimelineChanged(final Player player) { + public void onTimelineChanged(@NonNull final Player player) { publishFloatingQueueWindow(); } @Override - public void onCurrentWindowIndexChanged(final Player player) { + public void onCurrentMediaItemIndexChanged(@NonNull final Player player) { if (activeQueueItemId == MediaSessionCompat.QueueItem.UNKNOWN_ID || player.getCurrentTimeline().getWindowCount() > maxQueueSize) { publishFloatingQueueWindow(); } else if (!player.getCurrentTimeline().isEmpty()) { - activeQueueItemId = player.getCurrentWindowIndex(); + activeQueueItemId = player.getCurrentMediaItemIndex(); } } @@ -64,18 +63,17 @@ public class PlayQueueNavigator implements MediaSessionConnector.QueueNavigator } @Override - public void onSkipToPrevious(final Player player, final ControlDispatcher controlDispatcher) { + public void onSkipToPrevious(@NonNull final Player player) { callback.playPrevious(); } @Override - public void onSkipToQueueItem(final Player player, final ControlDispatcher controlDispatcher, - final long id) { + public void onSkipToQueueItem(@NonNull final Player player, final long id) { callback.playItemAtIndex((int) id); } @Override - public void onSkipToNext(final Player player, final ControlDispatcher controlDispatcher) { + public void onSkipToNext(@NonNull final Player player) { callback.playNext(); } @@ -102,8 +100,10 @@ public class PlayQueueNavigator implements MediaSessionConnector.QueueNavigator } @Override - public boolean onCommand(final Player player, final ControlDispatcher controlDispatcher, - final String command, final Bundle extras, final ResultReceiver cb) { + public boolean onCommand(@NonNull final Player player, + @NonNull final String command, + @Nullable final Bundle extras, + @Nullable final ResultReceiver cb) { return false; } } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueuePlaybackController.java b/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueuePlaybackController.java deleted file mode 100644 index 8bfbcde6b..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueuePlaybackController.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.schabi.newpipe.player.mediasession; - -import com.google.android.exoplayer2.DefaultControlDispatcher; -import com.google.android.exoplayer2.Player; - -public class PlayQueuePlaybackController extends DefaultControlDispatcher { - private final MediaSessionCallback callback; - - public PlayQueuePlaybackController(final MediaSessionCallback callback) { - super(); - this.callback = callback; - } - - @Override - public boolean dispatchSetPlayWhenReady(final Player player, final boolean playWhenReady) { - if (playWhenReady) { - callback.play(); - } else { - callback.pause(); - } - return true; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java index 7594f3a16..75fbbe433 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java @@ -2,52 +2,80 @@ package org.schabi.newpipe.player.mediasource; import android.util.Log; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - import com.google.android.exoplayer2.MediaItem; -import com.google.android.exoplayer2.source.BaseMediaSource; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.CompositeMediaSource; import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.SilenceMediaSource; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.TransferListener; +import org.schabi.newpipe.player.mediaitem.ExceptionTag; +import org.schabi.newpipe.player.mediaitem.MediaItemTag; import org.schabi.newpipe.player.playqueue.PlayQueueItem; import java.io.IOException; +import java.util.Collections; +import java.util.concurrent.TimeUnit; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public class FailedMediaSource extends CompositeMediaSource implements ManagedMediaSource { + private static final long SILENCE_DURATION_US = TimeUnit.SECONDS.toMicros(2); -public class FailedMediaSource extends BaseMediaSource implements ManagedMediaSource { private final String TAG = "FailedMediaSource@" + Integer.toHexString(hashCode()); private final PlayQueueItem playQueueItem; - private final FailedMediaSourceException error; + private final Throwable error; private final long retryTimestamp; - + private final MediaSource source; + private final MediaItem mediaItem; + /** + * Permanently fail the play queue item associated with this source, with no hope of retrying. + * + * The error will be propagated if the cause for load exception is unspecified. + * This means the error might be caused by reasons outside of extraction (e.g. no network). + * Otherwise, a silenced stream will play instead. + * + * @param playQueueItem play queue item + * @param error exception that was the reason to fail + * @param retryTimestamp epoch timestamp when this MediaSource can be refreshed + */ public FailedMediaSource(@NonNull final PlayQueueItem playQueueItem, - @NonNull final FailedMediaSourceException error, + @NonNull final Throwable error, final long retryTimestamp) { this.playQueueItem = playQueueItem; this.error = error; this.retryTimestamp = retryTimestamp; + + final MediaItemTag tag = ExceptionTag + .of(playQueueItem, Collections.singletonList(error)) + .withExtras(this); + this.mediaItem = tag.asMediaItem(); + this.source = new SilenceMediaSource.Factory() + .setDurationUs(SILENCE_DURATION_US) + .setTag(tag) + .createMediaSource(); } - /** - * Permanently fail the play queue item associated with this source, with no hope of retrying. - * The error will always be propagated to ExoPlayer. - * - * @param playQueueItem play queue item - * @param error exception that was the reason to fail - */ - public FailedMediaSource(@NonNull final PlayQueueItem playQueueItem, - @NonNull final FailedMediaSourceException error) { - this.playQueueItem = playQueueItem; - this.error = error; - this.retryTimestamp = Long.MAX_VALUE; + public static FailedMediaSource of(@NonNull final PlayQueueItem playQueueItem, + @NonNull final FailedMediaSourceException error) { + return new FailedMediaSource(playQueueItem, error, Long.MAX_VALUE); + } + + public static FailedMediaSource of(@NonNull final PlayQueueItem playQueueItem, + @NonNull final Throwable error, + final long retryWaitMillis) { + return new FailedMediaSource(playQueueItem, error, + System.currentTimeMillis() + retryWaitMillis); } public PlayQueueItem getStream() { return playQueueItem; } - public FailedMediaSourceException getError() { + public Throwable getError() { return error; } @@ -60,31 +88,46 @@ public class FailedMediaSource extends BaseMediaSource implements ManagedMediaSo */ @Override public MediaItem getMediaItem() { - return MediaItem.fromUri(playQueueItem.getUrl()); + return mediaItem; } @Override - public void maybeThrowSourceInfoRefreshError() throws IOException { - throw new IOException(error); + protected void prepareSourceInternal(@Nullable final TransferListener mediaTransferListener) { + super.prepareSourceInternal(mediaTransferListener); + Log.e(TAG, "Loading failed source: ", error); + if (error instanceof FailedMediaSourceException) { + prepareChildSource(null, source); + } } + + @Override + public void maybeThrowSourceInfoRefreshError() throws IOException { + if (!(error instanceof FailedMediaSourceException)) { + throw new IOException(error); + } + super.maybeThrowSourceInfoRefreshError(); + } + + @Override + protected void onChildSourceInfoRefreshed(final Void id, + final MediaSource mediaSource, + final Timeline timeline) { + refreshSourceInfo(timeline); + } + + @Override public MediaPeriod createPeriod(final MediaPeriodId id, final Allocator allocator, final long startPositionUs) { - return null; + return source.createPeriod(id, allocator, startPositionUs); } @Override - public void releasePeriod(final MediaPeriod mediaPeriod) { } - - @Override - protected void prepareSourceInternal(@Nullable final TransferListener mediaTransferListener) { - Log.e(TAG, "Loading failed source: ", error); + public void releasePeriod(final MediaPeriod mediaPeriod) { + source.releasePeriod(mediaPeriod); } - @Override - protected void releaseSourceInternal() { } - @Override public boolean shouldBeReplacedWith(@NonNull final PlayQueueItem newIdentity, final boolean isInterruptable) { diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java index 746a97581..193b90271 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java @@ -1,32 +1,34 @@ package org.schabi.newpipe.player.mediasource; -import android.os.Handler; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.CompositeMediaSource; +import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.upstream.TransferListener; + +import org.schabi.newpipe.player.mediaitem.MediaItemTag; +import org.schabi.newpipe.player.playqueue.PlayQueueItem; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.google.android.exoplayer2.MediaItem; -import com.google.android.exoplayer2.drm.DrmSessionEventListener; -import com.google.android.exoplayer2.source.MediaPeriod; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.MediaSourceEventListener; -import com.google.android.exoplayer2.upstream.Allocator; -import com.google.android.exoplayer2.upstream.TransferListener; - -import org.schabi.newpipe.player.playqueue.PlayQueueItem; - -import java.io.IOException; - -public class LoadedMediaSource implements ManagedMediaSource { +public class LoadedMediaSource extends CompositeMediaSource implements ManagedMediaSource { private final MediaSource source; private final PlayQueueItem stream; + private final MediaItem mediaItem; private final long expireTimestamp; - public LoadedMediaSource(@NonNull final MediaSource source, @NonNull final PlayQueueItem stream, + public LoadedMediaSource(@NonNull final MediaSource source, + @NonNull final MediaItemTag tag, + @NonNull final PlayQueueItem stream, final long expireTimestamp) { this.source = source; this.stream = stream; this.expireTimestamp = expireTimestamp; + + this.mediaItem = tag.withExtras(this).asMediaItem(); } public PlayQueueItem getStream() { @@ -38,19 +40,16 @@ public class LoadedMediaSource implements ManagedMediaSource { } @Override - public void prepareSource(final MediaSourceCaller mediaSourceCaller, - @Nullable final TransferListener mediaTransferListener) { - source.prepareSource(mediaSourceCaller, mediaTransferListener); + protected void prepareSourceInternal(@Nullable final TransferListener mediaTransferListener) { + super.prepareSourceInternal(mediaTransferListener); + prepareChildSource(null, source); } @Override - public void maybeThrowSourceInfoRefreshError() throws IOException { - source.maybeThrowSourceInfoRefreshError(); - } - - @Override - public void enable(final MediaSourceCaller caller) { - source.enable(caller); + protected void onChildSourceInfoRefreshed(final Void id, + final MediaSource mediaSource, + final Timeline timeline) { + refreshSourceInfo(timeline); } @Override @@ -64,57 +63,10 @@ public class LoadedMediaSource implements ManagedMediaSource { source.releasePeriod(mediaPeriod); } - @Override - public void disable(final MediaSourceCaller caller) { - source.disable(caller); - } - - @Override - public void releaseSource(final MediaSourceCaller mediaSourceCaller) { - source.releaseSource(mediaSourceCaller); - } - - @Override - public void addEventListener(final Handler handler, - final MediaSourceEventListener eventListener) { - source.addEventListener(handler, eventListener); - } - - @Override - public void removeEventListener(final MediaSourceEventListener eventListener) { - source.removeEventListener(eventListener); - } - - /** - * Adds a {@link DrmSessionEventListener} to the list of listeners which are notified of DRM - * events for this media source. - * - * @param handler A handler on the which listener events will be posted. - * @param eventListener The listener to be added. - */ - @Override - public void addDrmEventListener(final Handler handler, - final DrmSessionEventListener eventListener) { - source.addDrmEventListener(handler, eventListener); - } - - /** - * Removes a {@link DrmSessionEventListener} from the list of listeners which are notified of - * DRM events for this media source. - * - * @param eventListener The listener to be removed. - */ - @Override - public void removeDrmEventListener(final DrmSessionEventListener eventListener) { - source.removeDrmEventListener(eventListener); - } - - /** - * Returns the {@link MediaItem} whose media is provided by the source. - */ + @NonNull @Override public MediaItem getMediaItem() { - return source.getMediaItem(); + return mediaItem; } @Override diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSource.java index 21fddbe86..9d6b94893 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSource.java @@ -1,7 +1,6 @@ package org.schabi.newpipe.player.mediasource; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import com.google.android.exoplayer2.source.MediaSource; @@ -28,10 +27,4 @@ public interface ManagedMediaSource extends MediaSource { * @return whether this source is for the specified stream */ boolean isStreamEqual(@NonNull PlayQueueItem stream); - - @Nullable - @Override - default Object getTag() { - return this; - } } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSourcePlaylist.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSourcePlaylist.java index ff0cf21fa..4c0380767 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSourcePlaylist.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSourcePlaylist.java @@ -8,6 +8,8 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.ShuffleOrder; +import org.schabi.newpipe.player.mediaitem.MediaItemTag; + public class ManagedMediaSourcePlaylist { @NonNull private final ConcatenatingMediaSource internalSource; @@ -34,8 +36,14 @@ public class ManagedMediaSourcePlaylist { */ @Nullable public ManagedMediaSource get(final int index) { - return (index < 0 || index >= size()) - ? null : (ManagedMediaSource) internalSource.getMediaSource(index).getTag(); + if (index < 0 || index >= size()) { + return null; + } + + return MediaItemTag + .from(internalSource.getMediaSource(index).getMediaItem()) + .flatMap(tag -> tag.getMaybeExtras(ManagedMediaSource.class)) + .orElse(null); } @NonNull @@ -54,7 +62,7 @@ public class ManagedMediaSourcePlaylist { * @see #append(ManagedMediaSource) */ public synchronized void expand() { - append(new PlaceholderMediaSource()); + append(PlaceholderMediaSource.COPY); } /** @@ -115,10 +123,10 @@ public class ManagedMediaSourcePlaylist { public synchronized void invalidate(final int index, @Nullable final Handler handler, @Nullable final Runnable finalizingAction) { - if (get(index) instanceof PlaceholderMediaSource) { + if (get(index) == PlaceholderMediaSource.COPY) { return; } - update(index, new PlaceholderMediaSource(), handler, finalizingAction); + update(index, PlaceholderMediaSource.COPY, handler, finalizingAction); } /** diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java index 1cd855627..6b3f91eb3 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java @@ -1,28 +1,37 @@ package org.schabi.newpipe.player.mediasource; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - import com.google.android.exoplayer2.MediaItem; -import com.google.android.exoplayer2.source.BaseMediaSource; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.CompositeMediaSource; import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.upstream.Allocator; -import com.google.android.exoplayer2.upstream.TransferListener; +import org.schabi.newpipe.player.mediaitem.PlaceholderTag; import org.schabi.newpipe.player.playqueue.PlayQueueItem; -public class PlaceholderMediaSource extends BaseMediaSource implements ManagedMediaSource { +import androidx.annotation.NonNull; + +final class PlaceholderMediaSource + extends CompositeMediaSource implements ManagedMediaSource { + public static final PlaceholderMediaSource COPY = new PlaceholderMediaSource(); + private static final MediaItem MEDIA_ITEM = PlaceholderTag.EMPTY.withExtras(COPY).asMediaItem(); + + private PlaceholderMediaSource() { } /** * Returns the {@link MediaItem} whose media is provided by the source. */ @Override public MediaItem getMediaItem() { - return null; + return MEDIA_ITEM; } - // Do nothing, so this will stall the playback @Override - public void maybeThrowSourceInfoRefreshError() { } + protected void onChildSourceInfoRefreshed(final Void id, + final MediaSource mediaSource, + final Timeline timeline) { + /* Do nothing, no timeline updates will stall playback */ + } @Override public MediaPeriod createPeriod(final MediaPeriodId id, final Allocator allocator, @@ -33,12 +42,6 @@ public class PlaceholderMediaSource extends BaseMediaSource implements ManagedMe @Override public void releasePeriod(final MediaPeriod mediaPeriod) { } - @Override - protected void prepareSourceInternal(@Nullable final TransferListener mediaTransferListener) { } - - @Override - protected void releaseSourceInternal() { } - @Override public boolean shouldBeReplacedWith(@NonNull final PlayQueueItem newIdentity, final boolean isInterruptable) { diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/CustomTrackSelector.java b/app/src/main/java/org/schabi/newpipe/player/playback/CustomTrackSelector.java deleted file mode 100644 index 389be7062..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/playback/CustomTrackSelector.java +++ /dev/null @@ -1,92 +0,0 @@ -package org.schabi.newpipe.player.playback; - -import android.content.Context; -import android.text.TextUtils; -import android.util.Pair; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.RendererCapabilities.Capabilities; -import com.google.android.exoplayer2.source.TrackGroup; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.trackselection.ExoTrackSelection; -import com.google.android.exoplayer2.util.Assertions; - -/** - * This class allows irregular text language labels for use when selecting text captions and - * is mostly a copy-paste from {@link DefaultTrackSelector}. - *

- * This is a hack and should be removed once ExoPlayer fixes language normalization to accept - * a broader set of languages. - *

- */ -public class CustomTrackSelector extends DefaultTrackSelector { - private String preferredTextLanguage; - - public CustomTrackSelector(final Context context, - final ExoTrackSelection.Factory adaptiveTrackSelectionFactory) { - super(context, adaptiveTrackSelectionFactory); - } - - private static boolean formatHasLanguage(final Format format, final String language) { - return language != null && TextUtils.equals(language, format.language); - } - - public String getPreferredTextLanguage() { - return preferredTextLanguage; - } - - public void setPreferredTextLanguage(@NonNull final String label) { - Assertions.checkNotNull(label); - if (!label.equals(preferredTextLanguage)) { - preferredTextLanguage = label; - invalidate(); - } - } - - @Override - @Nullable - protected Pair selectTextTrack( - final TrackGroupArray groups, - @NonNull final int[][] formatSupport, - @NonNull final Parameters params, - @Nullable final String selectedAudioLanguage) { - TrackGroup selectedGroup = null; - int selectedTrackIndex = C.INDEX_UNSET; - TextTrackScore selectedTrackScore = null; - - for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) { - final TrackGroup trackGroup = groups.get(groupIndex); - @Capabilities final int[] trackFormatSupport = formatSupport[groupIndex]; - - for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { - if (isSupported(trackFormatSupport[trackIndex], - params.exceedRendererCapabilitiesIfNecessary)) { - final Format format = trackGroup.getFormat(trackIndex); - final TextTrackScore trackScore = new TextTrackScore(format, params, - trackFormatSupport[trackIndex], selectedAudioLanguage); - - if (formatHasLanguage(format, preferredTextLanguage)) { - selectedGroup = trackGroup; - selectedTrackIndex = trackIndex; - selectedTrackScore = trackScore; - break; // found user selected match (perfect!) - - } else if (trackScore.isWithinConstraints && (selectedTrackScore == null - || trackScore.compareTo(selectedTrackScore) > 0)) { - selectedGroup = trackGroup; - selectedTrackIndex = trackIndex; - selectedTrackScore = trackScore; - } - } - } - } - return selectedGroup == null ? null - : Pair.create(new ExoTrackSelection.Definition(selectedGroup, selectedTrackIndex), - Assertions.checkNotNull(selectedTrackScore)); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java index f3049d11d..d4ed973aa 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java @@ -11,11 +11,12 @@ import com.google.android.exoplayer2.source.MediaSource; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; +import org.schabi.newpipe.extractor.exceptions.ExtractionException; +import org.schabi.newpipe.player.mediaitem.MediaItemTag; import org.schabi.newpipe.player.mediasource.FailedMediaSource; import org.schabi.newpipe.player.mediasource.LoadedMediaSource; import org.schabi.newpipe.player.mediasource.ManagedMediaSource; import org.schabi.newpipe.player.mediasource.ManagedMediaSourcePlaylist; -import org.schabi.newpipe.player.mediasource.PlaceholderMediaSource; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueueItem; import org.schabi.newpipe.player.playqueue.events.MoveEvent; @@ -195,7 +196,7 @@ public class MediaSourceManager { //////////////////////////////////////////////////////////////////////////*/ private Subscriber getReactor() { - return new Subscriber() { + return new Subscriber<>() { @Override public void onSubscribe(@NonNull final Subscription d) { playQueueReactor.cancel(); @@ -209,10 +210,12 @@ public class MediaSourceManager { } @Override - public void onError(@NonNull final Throwable e) { } + public void onError(@NonNull final Throwable e) { + } @Override - public void onComplete() { } + public void onComplete() { + } }; } @@ -292,11 +295,11 @@ public class MediaSourceManager { } final ManagedMediaSource mediaSource = playlist.get(playQueue.getIndex()); - if (mediaSource == null) { + final PlayQueueItem playQueueItem = playQueue.getItem(); + if (mediaSource == null || playQueueItem == null) { return false; } - final PlayQueueItem playQueueItem = playQueue.getItem(); return mediaSource.isStreamEqual(playQueueItem); } @@ -315,7 +318,7 @@ public class MediaSourceManager { isBlocked.set(true); } - private void maybeUnblock() { + private boolean maybeUnblock() { if (DEBUG) { Log.d(TAG, "maybeUnblock() called."); } @@ -323,14 +326,17 @@ public class MediaSourceManager { if (isBlocked.get()) { isBlocked.set(false); playbackListener.onPlaybackUnblock(playlist.getParentMediaSource()); + return true; } + + return false; } /*////////////////////////////////////////////////////////////////////////// // Metadata Synchronization //////////////////////////////////////////////////////////////////////////*/ - private void maybeSync() { + private void maybeSync(final boolean wasBlocked) { if (DEBUG) { Log.d(TAG, "maybeSync() called."); } @@ -340,13 +346,13 @@ public class MediaSourceManager { return; } - playbackListener.onPlaybackSynchronize(currentItem); + playbackListener.onPlaybackSynchronize(currentItem, wasBlocked); } private synchronized void maybeSynchronizePlayer() { if (isPlayQueueReady() && isPlaybackReady()) { - maybeUnblock(); - maybeSync(); + final boolean isBlockReleased = maybeUnblock(); + maybeSync(isBlockReleased); } } @@ -417,20 +423,26 @@ public class MediaSourceManager { private Single getLoadedMediaSource(@NonNull final PlayQueueItem stream) { return stream.getStream().map(streamInfo -> { final MediaSource source = playbackListener.sourceOf(stream, streamInfo); - if (source == null) { + if (source == null || !MediaItemTag.from(source.getMediaItem()).isPresent()) { 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)); + return (ManagedMediaSource) + FailedMediaSource.of(stream, new MediaSourceResolutionException(message)); } + final MediaItemTag tag = MediaItemTag.from(source.getMediaItem()).get(); final long expiration = System.currentTimeMillis() + ServiceHelper.getCacheExpirationMillis(streamInfo.getServiceId()); - return new LoadedMediaSource(source, stream, expiration); - }).onErrorReturn(throwable -> new FailedMediaSource(stream, - new StreamInfoLoadException(throwable))); + return new LoadedMediaSource(source, tag, stream, expiration); + }).onErrorReturn(throwable -> { + if (throwable instanceof ExtractionException) { + return FailedMediaSource.of(stream, new StreamInfoLoadException(throwable)); + } + return FailedMediaSource.of(stream, throwable, /*immediatelyRetryable=*/0L); + }); } private void onMediaSourceReceived(@NonNull final PlayQueueItem item, @@ -478,23 +490,23 @@ public class MediaSourceManager { /** * Checks if the current playing index contains an expired {@link ManagedMediaSource}. - * If so, the expired source is replaced by a {@link PlaceholderMediaSource} and + * If so, the expired source is replaced by a dummy {@link ManagedMediaSource} and * {@link #loadImmediate()} is called to reload the current item. *

* If not, then the media source at the current index is ready for playback, and * {@link #maybeSynchronizePlayer()} is called. *

- * Under both cases, {@link #maybeSync()} will be called to ensure the listener + * Under both cases, {@link #maybeSync(boolean)} will be called to ensure the listener * is up-to-date. */ private void maybeRenewCurrentIndex() { final int currentIndex = playQueue.getIndex(); + final PlayQueueItem currentItem = playQueue.getItem(); final ManagedMediaSource currentSource = playlist.get(currentIndex); - if (currentSource == null) { + if (currentItem == null || currentSource == null) { return; } - final PlayQueueItem currentItem = playQueue.getItem(); if (!currentSource.shouldBeReplacedWith(currentItem, true)) { maybeSynchronizePlayer(); return; diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java b/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java index 811f82b3b..737607001 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java @@ -51,9 +51,10 @@ public interface PlaybackListener { * May be called anytime at any amount once unblock is called. *

* - * @param item + * @param item item the player should be playing/synchronized to + * @param wasBlocked was the player recently released from blocking state */ - void onPlaybackSynchronize(@NonNull PlayQueueItem item); + void onPlaybackSynchronize(@NonNull PlayQueueItem item, boolean wasBlocked); /** * Requests the listener to resolve a stream info into a media source diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/SurfaceHolderCallback.java b/app/src/main/java/org/schabi/newpipe/player/playback/SurfaceHolderCallback.java index 0814092fa..5d67e6967 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/SurfaceHolderCallback.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/SurfaceHolderCallback.java @@ -3,7 +3,7 @@ package org.schabi.newpipe.player.playback; import android.content.Context; import android.view.SurfaceHolder; -import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.video.DummySurface; /** @@ -25,10 +25,10 @@ import com.google.android.exoplayer2.video.DummySurface; public final class SurfaceHolderCallback implements SurfaceHolder.Callback { private final Context context; - private final SimpleExoPlayer player; + private final Player player; private DummySurface dummySurface; - public SurfaceHolderCallback(final Context context, final SimpleExoPlayer player) { + public SurfaceHolderCallback(final Context context, final Player player) { this.context = context; this.player = player; } diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java index 29be402c5..9bded9331 100644 --- a/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java +++ b/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java @@ -12,6 +12,8 @@ import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.player.helper.PlayerDataSource; import org.schabi.newpipe.player.helper.PlayerHelper; +import org.schabi.newpipe.player.mediaitem.MediaItemTag; +import org.schabi.newpipe.player.mediaitem.StreamInfoTag; import org.schabi.newpipe.util.ListHelper; public class AudioPlaybackResolver implements PlaybackResolver { @@ -40,7 +42,7 @@ public class AudioPlaybackResolver implements PlaybackResolver { } final AudioStream audio = info.getAudioStreams().get(index); - final MediaSourceTag tag = new MediaSourceTag(info); + final MediaItemTag tag = StreamInfoTag.of(info); return buildMediaSource(dataSource, audio.getUrl(), PlayerHelper.cacheKeyOf(info, audio), MediaFormat.getSuffixById(audio.getFormatId()), tag); } diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/MediaSourceTag.java b/app/src/main/java/org/schabi/newpipe/player/resolver/MediaSourceTag.java deleted file mode 100644 index 360e92e7f..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/resolver/MediaSourceTag.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.schabi.newpipe.player.resolver; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.schabi.newpipe.extractor.stream.StreamInfo; -import org.schabi.newpipe.extractor.stream.VideoStream; - -import java.io.Serializable; -import java.util.Collections; -import java.util.List; - -public class MediaSourceTag implements Serializable { - @NonNull - private final StreamInfo metadata; - - @NonNull - private final List sortedAvailableVideoStreams; - private final int selectedVideoStreamIndex; - - public MediaSourceTag(@NonNull final StreamInfo metadata, - @NonNull final List sortedAvailableVideoStreams, - final int selectedVideoStreamIndex) { - this.metadata = metadata; - this.sortedAvailableVideoStreams = sortedAvailableVideoStreams; - this.selectedVideoStreamIndex = selectedVideoStreamIndex; - } - - public MediaSourceTag(@NonNull final StreamInfo metadata) { - this(metadata, Collections.emptyList(), /*indexNotAvailable=*/-1); - } - - @NonNull - public StreamInfo getMetadata() { - return metadata; - } - - @NonNull - public List getSortedAvailableVideoStreams() { - return sortedAvailableVideoStreams; - } - - public int getSelectedVideoStreamIndex() { - return selectedVideoStreamIndex; - } - - @Nullable - public VideoStream getSelectedVideoStream() { - return selectedVideoStreamIndex < 0 - || selectedVideoStreamIndex >= sortedAvailableVideoStreams.size() - ? null : sortedAvailableVideoStreams.get(selectedVideoStreamIndex); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java index cfe9dbb62..90b38ed51 100644 --- a/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java +++ b/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java @@ -3,20 +3,23 @@ package org.schabi.newpipe.player.resolver; import android.net.Uri; import android.text.TextUtils; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.MediaSourceFactory; import com.google.android.exoplayer2.util.Util; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.player.helper.PlayerDataSource; +import org.schabi.newpipe.player.mediaitem.MediaItemTag; +import org.schabi.newpipe.player.mediaitem.StreamInfoTag; import org.schabi.newpipe.util.StreamTypeUtil; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import static org.schabi.newpipe.player.helper.PlayerDataSource.LIVE_STREAM_EDGE_GAP_MILLIS; + public interface PlaybackResolver extends Resolver { @Nullable @@ -27,7 +30,7 @@ public interface PlaybackResolver extends Resolver { return null; } - final MediaSourceTag tag = new MediaSourceTag(info); + final StreamInfoTag tag = StreamInfoTag.of(info); if (!info.getHlsUrl().isEmpty()) { return buildLiveMediaSource(dataSource, info.getHlsUrl(), C.TYPE_HLS, tag); } else if (!info.getDashMpdUrl().isEmpty()) { @@ -41,8 +44,8 @@ public interface PlaybackResolver extends Resolver { default MediaSource buildLiveMediaSource(@NonNull final PlayerDataSource dataSource, @NonNull final String sourceUrl, @C.ContentType final int type, - @NonNull final MediaSourceTag metadata) { - final MediaSourceFactory factory; + @NonNull final MediaItemTag metadata) { + final MediaSource.Factory factory; switch (type) { case C.TYPE_SS: factory = dataSource.getLiveSsMediaSourceFactory(); @@ -61,7 +64,11 @@ public interface PlaybackResolver extends Resolver { new MediaItem.Builder() .setTag(metadata) .setUri(Uri.parse(sourceUrl)) - .setLiveTargetOffsetMs(PlayerDataSource.LIVE_STREAM_EDGE_GAP_MILLIS) + .setLiveConfiguration( + new MediaItem.LiveConfiguration.Builder() + .setTargetOffsetMs(LIVE_STREAM_EDGE_GAP_MILLIS) + .build() + ) .build() ); } @@ -71,12 +78,12 @@ public interface PlaybackResolver extends Resolver { @NonNull final String sourceUrl, @NonNull final String cacheKey, @NonNull final String overrideExtension, - @NonNull final MediaSourceTag metadata) { + @NonNull final MediaItemTag metadata) { final Uri uri = Uri.parse(sourceUrl); @C.ContentType final int type = TextUtils.isEmpty(overrideExtension) ? Util.inferContentType(uri) : Util.inferContentType("." + overrideExtension); - final MediaSourceFactory factory; + final MediaSource.Factory factory; switch (type) { case C.TYPE_SS: factory = dataSource.getLiveSsMediaSourceFactory(); diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java index 11949f55d..565f0b23e 100644 --- a/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java +++ b/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java @@ -17,6 +17,8 @@ import org.schabi.newpipe.extractor.stream.SubtitlesStream; import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.player.helper.PlayerDataSource; import org.schabi.newpipe.player.helper.PlayerHelper; +import org.schabi.newpipe.player.mediaitem.MediaItemTag; +import org.schabi.newpipe.player.mediaitem.StreamInfoTag; import org.schabi.newpipe.util.ListHelper; import java.util.ArrayList; @@ -73,8 +75,10 @@ public class VideoPlaybackResolver implements PlaybackResolver { } else { index = qualityResolver.getOverrideResolutionIndex(videos, getPlaybackQuality()); } - final MediaSourceTag tag = new MediaSourceTag(info, videos, index); - @Nullable final VideoStream video = tag.getSelectedVideoStream(); + final MediaItemTag tag = StreamInfoTag.of(info, videos, index); + @Nullable final VideoStream video = tag.getMaybeQuality() + .map(MediaItemTag.Quality::getSelectedVideoStream) + .orElse(null); if (video != null) { final MediaSource streamSource = buildMediaSource(dataSource, video.getUrl(), @@ -112,12 +116,14 @@ public class VideoPlaybackResolver implements PlaybackResolver { if (mimeType == null) { continue; } - final MediaSource textSource = dataSource.getSampleMediaSourceFactory() - .createMediaSource( - new MediaItem.Subtitle(Uri.parse(subtitle.getUrl()), - mimeType, - PlayerHelper.captionLanguageOf(context, subtitle)), - TIME_UNSET); + final MediaItem.SubtitleConfiguration textMediaItem = + new MediaItem.SubtitleConfiguration.Builder(Uri.parse(subtitle.getUrl())) + .setMimeType(mimeType) + .setLanguage(PlayerHelper.captionLanguageOf(context, subtitle)) + .build(); + final MediaSource textSource = dataSource + .getSampleMediaSourceFactory() + .createMediaSource(textMediaItem, TIME_UNSET); mediaSources.add(textSource); } } From 69646e5b5d5adae43abafcfa47b2aa587665ddb7 Mon Sep 17 00:00:00 2001 From: karyogamy Date: Wed, 16 Mar 2022 20:47:29 -0400 Subject: [PATCH 2/5] added: documentations to MediaItemTags and Player. fixed: checkStyle failures. --- .../org/schabi/newpipe/player/Player.java | 53 ++++++++++++------- .../player/mediaitem/ExceptionTag.java | 10 ++++ .../player/mediaitem/MediaItemTag.java | 8 +++ .../player/mediaitem/PlaceholderTag.java | 11 +++- .../player/mediaitem/StreamInfoTag.java | 8 +++ .../player/mediasource/FailedMediaSource.java | 12 ++++- 6 files changed, 79 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java index 2305eb9d0..355cd4079 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -133,7 +133,6 @@ import com.google.android.exoplayer2.PlaybackException; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player.PositionInfo; import com.google.android.exoplayer2.RenderersFactory; -import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.TracksInfo; import com.google.android.exoplayer2.source.MediaSource; @@ -2487,6 +2486,7 @@ public final class Player implements //////////////////////////////////////////////////////////////////////////*/ //region ExoPlayer listeners (that didn't fit in other categories) + @Override public void onEvents(@NonNull final com.google.android.exoplayer2.Player player, @NonNull final com.google.android.exoplayer2.Player.Events events) { Listener.super.onEvents(player, events); @@ -2546,14 +2546,6 @@ public final class Player implements return; } - if (newPosition.contentPositionMs == 0 && - simpleExoPlayer.getTotalBufferedDuration() < 500L) { - Log.d(TAG, "Playback - skipping to initial keyframe."); - simpleExoPlayer.setSeekParameters(SeekParameters.CLOSEST_SYNC); - simpleExoPlayer.seekTo(1L); - simpleExoPlayer.setSeekParameters(PlayerHelper.getSeekParameters(context)); - } - // Refresh the playback if there is a transition to the next video final int newIndex = newPosition.mediaItemIndex; switch (discontinuityReason) { @@ -2605,7 +2597,29 @@ public final class Player implements //region Errors /** * Process exceptions produced by {@link com.google.android.exoplayer2.ExoPlayer ExoPlayer}. - * + *

There are multiple types of errors:

+ *
    + *
  • {@link PlaybackException#ERROR_CODE_BEHIND_LIVE_WINDOW BEHIND_LIVE_WINDOW}: + * If the playback on livestreams are lagged too far behind the current playable + * window. Then we seek to the latest timestamp and restart the playback. + *
  • + *
  • From {@link PlaybackException#ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE BAD_IO} to + * {@link PlaybackException#ERROR_CODE_PARSING_MANIFEST_UNSUPPORTED UNSUPPORTED_FORMATS}: + * If the stream source is validated by the extractor but not recognized by the player, + * then we can try to recover playback by signal an error on the {@link PlayQueue}.
  • + *
  • For {@link PlaybackException#ERROR_CODE_TIMEOUT PLAYER_TIMEOUT}, + * {@link PlaybackException#ERROR_CODE_IO_UNSPECIFIED MEDIA_SOURCE_RESOLVER_TIMEOUT} and + * {@link PlaybackException#ERROR_CODE_IO_NETWORK_CONNECTION_FAILED NO_NETWORK}: + * We can keep set the recovery record and keep to player at the current state until + * it is ready to play by restarting the {@link MediaSourceManager}.
  • + *
  • On any ExoPlayer specific issue internal to its device interaction, such as + * {@link PlaybackException#ERROR_CODE_DECODER_INIT_FAILED DECODER_ERROR}: + * We terminate the playback.
  • + *
  • For any other unspecified issue internal: We set a recovery and try to restart + * the playback.
  • + * In the case of decoder/renderer or unspecified errors, the player will create a + * notification so the users are aware. + *
* @see com.google.android.exoplayer2.Player.Listener#onPlayerError(PlaybackException) * */ @SuppressLint("SwitchIntDef") @@ -2648,6 +2662,9 @@ public final class Player implements case ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT: // Don't create notification on timeout/networking errors: isCatchableException = true; + setRecovery(); + reloadPlayQueueManager(); + break; case ERROR_CODE_UNSPECIFIED: // Reload playback on unexpected errors: setRecovery(); @@ -2749,7 +2766,6 @@ public final class Player implements return; } - final boolean onPlaybackInitial = currentItem == null; final boolean hasPlayQueueItemChanged = currentItem != item; final int currentPlayQueueIndex = playQueue.indexOf(item); @@ -2953,10 +2969,8 @@ public final class Player implements //region StreamInfo history: views and progress private void registerStreamViewed() { - getCurrentStreamInfo().ifPresent(info -> { - databaseUpdateDisposable - .add(recordManager.onViewed(info).onErrorComplete().subscribe()); - }); + getCurrentStreamInfo().ifPresent(info -> databaseUpdateDisposable + .add(recordManager.onViewed(info).onErrorComplete().subscribe())); } private void saveStreamProgressState(final long progressMillis) { @@ -3134,7 +3148,7 @@ public final class Player implements return; } - if (playQueue.getIndex() == index && simpleExoPlayer.getCurrentWindowIndex() == index) { + if (playQueue.getIndex() == index && simpleExoPlayer.getCurrentMediaItemIndex() == index) { seekToDefault(); } else { saveStreamProgressState(); @@ -3880,9 +3894,10 @@ public final class Player implements } private void onOpenInBrowserClicked() { - getCurrentStreamInfo().map(Info::getOriginalUrl).ifPresent(originalUrl -> { - ShareUtils.openUrlInBrowser(Objects.requireNonNull(getParentActivity()), originalUrl); - }); + getCurrentStreamInfo() + .map(Info::getOriginalUrl) + .ifPresent(originalUrl -> ShareUtils.openUrlInBrowser( + Objects.requireNonNull(getParentActivity()), originalUrl)); } //endregion diff --git a/app/src/main/java/org/schabi/newpipe/player/mediaitem/ExceptionTag.java b/app/src/main/java/org/schabi/newpipe/player/mediaitem/ExceptionTag.java index 2deffcf65..3e41262d6 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediaitem/ExceptionTag.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediaitem/ExceptionTag.java @@ -10,6 +10,16 @@ import java.util.Optional; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +/** + * This {@link MediaItemTag} object is designed to contain metadata for a stream + * that has failed to load. It supplies metadata from an underlying + * {@link PlayQueueItem}, which is used by the internal players to resolve actual + * playback info. + * + * This {@link MediaItemTag} does not contain any {@link StreamInfo} that can be + * used to start playback and can be detected by checking {@link ExceptionTag#getErrors()} + * when in generic form. + **/ public final class ExceptionTag implements MediaItemTag { @NonNull private final PlayQueueItem item; diff --git a/app/src/main/java/org/schabi/newpipe/player/mediaitem/MediaItemTag.java b/app/src/main/java/org/schabi/newpipe/player/mediaitem/MediaItemTag.java index 872a10a57..6f9b9e9f2 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediaitem/MediaItemTag.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediaitem/MediaItemTag.java @@ -4,6 +4,7 @@ import android.net.Uri; import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.MediaMetadata; +import com.google.android.exoplayer2.Player; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamType; @@ -16,6 +17,13 @@ import java.util.UUID; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +/** + * Metadata container and accessor used by player internals. + * + * This interface ensures consistency of fetching metadata on each stream, + * which is encapsulated in a {@link MediaItem} and delivered via ExoPlayer's + * {@link Player.Listener} on event triggers to the downstream users. + **/ public interface MediaItemTag { List getErrors(); diff --git a/app/src/main/java/org/schabi/newpipe/player/mediaitem/PlaceholderTag.java b/app/src/main/java/org/schabi/newpipe/player/mediaitem/PlaceholderTag.java index c4998e9af..5b53e5660 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediaitem/PlaceholderTag.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediaitem/PlaceholderTag.java @@ -2,6 +2,7 @@ package org.schabi.newpipe.player.mediaitem; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamType; +import org.schabi.newpipe.util.Constants; import java.util.Collections; import java.util.List; @@ -10,6 +11,12 @@ import java.util.Optional; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +/** + * This is a Placeholding {@link MediaItemTag}, designed as a dummy metadata object for + * any stream that has not been resolved. + * + * This object cannot be instantiated and does not hold real metadata of any form. + * */ public final class PlaceholderTag implements MediaItemTag { public static final PlaceholderTag EMPTY = new PlaceholderTag(null); private static final String UNKNOWN_VALUE_INTERNAL = "Placeholder"; @@ -29,7 +36,7 @@ public final class PlaceholderTag implements MediaItemTag { @Override public int getServiceId() { - return -1; + return Constants.NO_SERVICE_ID; } @Override @@ -44,7 +51,7 @@ public final class PlaceholderTag implements MediaItemTag { @Override public long getDurationSeconds() { - return -1; + return 0; } @Override diff --git a/app/src/main/java/org/schabi/newpipe/player/mediaitem/StreamInfoTag.java b/app/src/main/java/org/schabi/newpipe/player/mediaitem/StreamInfoTag.java index 93cf081f9..9ae25e07b 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediaitem/StreamInfoTag.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediaitem/StreamInfoTag.java @@ -1,5 +1,7 @@ package org.schabi.newpipe.player.mediaitem; +import com.google.android.exoplayer2.MediaItem; + import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.extractor.stream.VideoStream; @@ -11,6 +13,12 @@ import java.util.Optional; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +/** + * This {@link MediaItemTag} object contains metadata for a resolved stream + * that is ready for playback. This object guarantees the {@link StreamInfo} + * is available and may provide the {@link Quality} of video stream used in + * the {@link MediaItem}. + **/ public final class StreamInfoTag implements MediaItemTag { @NonNull private final StreamInfo streamInfo; diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java index 75fbbe433..d68eb1a1a 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java @@ -23,7 +23,15 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; public class FailedMediaSource extends CompositeMediaSource implements ManagedMediaSource { - private static final long SILENCE_DURATION_US = TimeUnit.SECONDS.toMicros(2); + /** + * Play 2 seconds of silenced audio when a stream fails to resolve due to a known issue, + * such as {@link org.schabi.newpipe.extractor.exceptions.ExtractionException}. + * + * This silence duration allows user to react and have time to jump to a previous stream, + * while still provide a smooth playback experience. A duration lower than 1 second is + * not recommended, it may cause ExoPlayer to buffer for a while. + * */ + public static final long SILENCE_DURATION_US = TimeUnit.SECONDS.toMicros(2); private final String TAG = "FailedMediaSource@" + Integer.toHexString(hashCode()); private final PlayQueueItem playQueueItem; @@ -32,7 +40,7 @@ public class FailedMediaSource extends CompositeMediaSource implements Man private final MediaSource source; private final MediaItem mediaItem; /** - * Permanently fail the play queue item associated with this source, with no hope of retrying. + * Fail the play queue item associated with this source, with potential future retries. * * The error will be propagated if the cause for load exception is unspecified. * This means the error might be caused by reasons outside of extraction (e.g. no network). From b81eb35f3d045e9bbaa77e8fcfc13ab1fb43cd09 Mon Sep 17 00:00:00 2001 From: karyogamy Date: Thu, 17 Mar 2022 22:56:22 -0400 Subject: [PATCH 3/5] added: documentations on lifecycles for FailedMediaSource and LoadedMediaSource. fixed: onPlaybackSynchronize to rewind when not playing, which was incorrectly removed in previous commit. fixed: sonar and checkstyle issues. --- .../org/schabi/newpipe/player/Player.java | 2 +- .../player/mediaitem/ExceptionTag.java | 10 -- .../player/mediaitem/MediaItemTag.java | 12 +- .../player/mediaitem/PlaceholderTag.java | 11 -- .../player/mediaitem/StreamInfoTag.java | 2 + .../player/mediasource/FailedMediaSource.java | 103 ++++++++++++------ .../player/mediasource/LoadedMediaSource.java | 39 ++++++- .../mediasource/PlaceholderMediaSource.java | 6 +- 8 files changed, 122 insertions(+), 63 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java index 355cd4079..ea0cab5e6 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -2791,7 +2791,7 @@ public final class Player implements + "index=[" + currentPlayQueueIndex + "] with " + "playlist length=[" + currentPlaylistSize + "]"); - } else if (wasBlocked || currentPlaylistIndex != currentPlayQueueIndex) { + } else if (wasBlocked || currentPlaylistIndex != currentPlayQueueIndex || !isPlaying()) { if (DEBUG) { Log.d(TAG, "Playback - Rewinding to correct " + "index=[" + currentPlayQueueIndex + "], " diff --git a/app/src/main/java/org/schabi/newpipe/player/mediaitem/ExceptionTag.java b/app/src/main/java/org/schabi/newpipe/player/mediaitem/ExceptionTag.java index 3e41262d6..7e28eda20 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediaitem/ExceptionTag.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediaitem/ExceptionTag.java @@ -87,16 +87,6 @@ public final class ExceptionTag implements MediaItemTag { return item.getStreamType(); } - @Override - public Optional getMaybeStreamInfo() { - return Optional.empty(); - } - - @Override - public Optional getMaybeQuality() { - return Optional.empty(); - } - @Override public Optional getMaybeExtras(@NonNull final Class type) { return Optional.ofNullable(extras).map(type::cast); diff --git a/app/src/main/java/org/schabi/newpipe/player/mediaitem/MediaItemTag.java b/app/src/main/java/org/schabi/newpipe/player/mediaitem/MediaItemTag.java index 6f9b9e9f2..8dbd55280 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediaitem/MediaItemTag.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediaitem/MediaItemTag.java @@ -44,9 +44,15 @@ public interface MediaItemTag { StreamType getStreamType(); - Optional getMaybeStreamInfo(); + @NonNull + default Optional getMaybeStreamInfo() { + return Optional.empty(); + } - Optional getMaybeQuality(); + @NonNull + default Optional getMaybeQuality() { + return Optional.empty(); + } Optional getMaybeExtras(@NonNull Class type); @@ -86,7 +92,7 @@ public interface MediaItemTag { .build(); } - class Quality { + final class Quality { @NonNull private final List sortedVideoStreams; private final int selectedVideoStreamIndex; diff --git a/app/src/main/java/org/schabi/newpipe/player/mediaitem/PlaceholderTag.java b/app/src/main/java/org/schabi/newpipe/player/mediaitem/PlaceholderTag.java index 5b53e5660..fe7aa9d92 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediaitem/PlaceholderTag.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediaitem/PlaceholderTag.java @@ -1,6 +1,5 @@ package org.schabi.newpipe.player.mediaitem; -import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.util.Constants; @@ -74,16 +73,6 @@ public final class PlaceholderTag implements MediaItemTag { return StreamType.NONE; } - @Override - public Optional getMaybeStreamInfo() { - return Optional.empty(); - } - - @Override - public Optional getMaybeQuality() { - return Optional.empty(); - } - @Override public Optional getMaybeExtras(@NonNull final Class type) { return Optional.ofNullable(extras).map(type::cast); diff --git a/app/src/main/java/org/schabi/newpipe/player/mediaitem/StreamInfoTag.java b/app/src/main/java/org/schabi/newpipe/player/mediaitem/StreamInfoTag.java index 9ae25e07b..6a942840d 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediaitem/StreamInfoTag.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediaitem/StreamInfoTag.java @@ -91,11 +91,13 @@ public final class StreamInfoTag implements MediaItemTag { return streamInfo.getStreamType(); } + @NonNull @Override public Optional getMaybeStreamInfo() { return Optional.of(streamInfo); } + @NonNull @Override public Optional getMaybeQuality() { return Optional.ofNullable(quality); diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java index d68eb1a1a..299ae845d 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java @@ -3,16 +3,16 @@ package org.schabi.newpipe.player.mediasource; import android.util.Log; import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.PlaybackException; import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.CompositeMediaSource; +import com.google.android.exoplayer2.source.BaseMediaSource; import com.google.android.exoplayer2.source.MediaPeriod; -import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.SilenceMediaSource; +import com.google.android.exoplayer2.source.SinglePeriodTimeline; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.TransferListener; import org.schabi.newpipe.player.mediaitem.ExceptionTag; -import org.schabi.newpipe.player.mediaitem.MediaItemTag; import org.schabi.newpipe.player.playqueue.PlayQueueItem; import java.io.IOException; @@ -22,7 +22,7 @@ import java.util.concurrent.TimeUnit; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -public class FailedMediaSource extends CompositeMediaSource implements ManagedMediaSource { +public class FailedMediaSource extends BaseMediaSource implements ManagedMediaSource { /** * Play 2 seconds of silenced audio when a stream fails to resolve due to a known issue, * such as {@link org.schabi.newpipe.extractor.exceptions.ExtractionException}. @@ -32,12 +32,12 @@ public class FailedMediaSource extends CompositeMediaSource implements Man * not recommended, it may cause ExoPlayer to buffer for a while. * */ public static final long SILENCE_DURATION_US = TimeUnit.SECONDS.toMicros(2); + public static final MediaPeriod SILENT_MEDIA = makeSilentMediaPeriod(SILENCE_DURATION_US); private final String TAG = "FailedMediaSource@" + Integer.toHexString(hashCode()); private final PlayQueueItem playQueueItem; private final Throwable error; private final long retryTimestamp; - private final MediaSource source; private final MediaItem mediaItem; /** * Fail the play queue item associated with this source, with potential future retries. @@ -56,15 +56,10 @@ public class FailedMediaSource extends CompositeMediaSource implements Man this.playQueueItem = playQueueItem; this.error = error; this.retryTimestamp = retryTimestamp; - - final MediaItemTag tag = ExceptionTag + this.mediaItem = ExceptionTag .of(playQueueItem, Collections.singletonList(error)) - .withExtras(this); - this.mediaItem = tag.asMediaItem(); - this.source = new SilenceMediaSource.Factory() - .setDurationUs(SILENCE_DURATION_US) - .setTag(tag) - .createMediaSource(); + .withExtras(this) + .asMediaItem(); } public static FailedMediaSource of(@NonNull final PlayQueueItem playQueueItem, @@ -91,49 +86,77 @@ public class FailedMediaSource extends CompositeMediaSource implements Man return System.currentTimeMillis() >= retryTimestamp; } - /** - * Returns the {@link MediaItem} whose media is provided by the source. - */ @Override public MediaItem getMediaItem() { return mediaItem; } + /** + * Prepares the source with {@link Timeline} info on the silence playback when the error + * is classed as {@link FailedMediaSourceException}, for example, when the error is + * {@link org.schabi.newpipe.extractor.exceptions.ExtractionException ExtractionException}. + * These types of error are swallowed by {@link FailedMediaSource}, and the underlying + * exception is carried to the {@link MediaItem} metadata during playback. + *

+ * If the exception is not known, e.g. {@link java.net.UnknownHostException} or some + * other network issue, then no source info is refreshed and + * {@link #maybeThrowSourceInfoRefreshError()} be will triggered. + *

+ * Note that this method is called only once until {@link #releaseSourceInternal()} is called, + * so if no action is done in here, playback will stall unless + * {@link #maybeThrowSourceInfoRefreshError()} is called. + * + * @param mediaTransferListener No data transfer listener needed, ignored here. + */ @Override protected void prepareSourceInternal(@Nullable final TransferListener mediaTransferListener) { - super.prepareSourceInternal(mediaTransferListener); Log.e(TAG, "Loading failed source: ", error); if (error instanceof FailedMediaSourceException) { - prepareChildSource(null, source); + refreshSourceInfo(makeSilentMediaTimeline(SILENCE_DURATION_US, mediaItem)); } } - + /** + * If the error is not known, e.g. network issue, then the exception is not swallowed here in + * {@link FailedMediaSource}. The exception is then propagated to the player, which + * {@link org.schabi.newpipe.player.Player Player} can react to inside + * {@link com.google.android.exoplayer2.Player.Listener#onPlayerError(PlaybackException)}. + * + * @throws IOException An error which will always result in + * {@link com.google.android.exoplayer2.PlaybackException#ERROR_CODE_IO_UNSPECIFIED}. + */ @Override public void maybeThrowSourceInfoRefreshError() throws IOException { if (!(error instanceof FailedMediaSourceException)) { throw new IOException(error); } - super.maybeThrowSourceInfoRefreshError(); } + /** + * This method is only called if {@link #prepareSourceInternal(TransferListener)} + * refreshes the source info with no exception. All parameters are ignored as this + * returns a static and reused piece of silent audio. + * + * @param id The identifier of the period. + * @param allocator An {@link Allocator} from which to obtain media buffer allocations. + * @param startPositionUs The expected start position, in microseconds. + * @return The common {@link MediaPeriod} holding the silence. + */ @Override - protected void onChildSourceInfoRefreshed(final Void id, - final MediaSource mediaSource, - final Timeline timeline) { - refreshSourceInfo(timeline); - } - - - @Override - public MediaPeriod createPeriod(final MediaPeriodId id, final Allocator allocator, + public MediaPeriod createPeriod(final MediaPeriodId id, + final Allocator allocator, final long startPositionUs) { - return source.createPeriod(id, allocator, startPositionUs); + return SILENT_MEDIA; } @Override public void releasePeriod(final MediaPeriod mediaPeriod) { - source.releasePeriod(mediaPeriod); + /* Do Nothing (we want to keep re-using the Silent MediaPeriod) */ + } + + @Override + protected void releaseSourceInternal() { + /* Do Nothing, no clean-up for processing/extra thread is needed by this MediaSource */ } @Override @@ -168,4 +191,22 @@ public class FailedMediaSource extends CompositeMediaSource implements Man super(cause); } } + + private static Timeline makeSilentMediaTimeline(final long durationUs, + @NonNull final MediaItem mediaItem) { + return new SinglePeriodTimeline( + durationUs, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* useLiveConfiguration= */ false, + /* manifest= */ null, + mediaItem); + } + + private static MediaPeriod makeSilentMediaPeriod(final long durationUs) { + return new SilenceMediaSource.Factory() + .setDurationUs(durationUs) + .createMediaSource() + .createPeriod(null, null, 0); + } } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java index 193b90271..95524cf69 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java @@ -14,12 +14,24 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItem; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -public class LoadedMediaSource extends CompositeMediaSource implements ManagedMediaSource { +public class LoadedMediaSource extends CompositeMediaSource implements ManagedMediaSource { private final MediaSource source; private final PlayQueueItem stream; private final MediaItem mediaItem; private final long expireTimestamp; + /** + * Uses a {@link CompositeMediaSource} to wrap one or more child {@link MediaSource}s + * containing actual media. This wrapper {@link LoadedMediaSource} holds the expiration + * timestamp as a {@link ManagedMediaSource} to allow explicit playlist management under + * {@link ManagedMediaSourcePlaylist}. + * + * @param source The child media source with actual media. + * @param tag Metadata for the child media source. + * @param stream The queue item associated with the media source. + * @param expireTimestamp The timestamp when the media source expires and might not be + * available for playback. + */ public LoadedMediaSource(@NonNull final MediaSource source, @NonNull final MediaItemTag tag, @NonNull final PlayQueueItem stream, @@ -39,14 +51,35 @@ public class LoadedMediaSource extends CompositeMediaSource implements Man return System.currentTimeMillis() >= expireTimestamp; } + /** + * Delegates the preparation of child {@link MediaSource}s to the + * {@link CompositeMediaSource} wrapper. Since all {@link LoadedMediaSource}s use only + * a single child media, the child id of 0 is always used (sonar doesn't like null as id here). + * + * @param mediaTransferListener A data transfer listener that will be registered by the + * {@link CompositeMediaSource} for child source preparation. + */ @Override protected void prepareSourceInternal(@Nullable final TransferListener mediaTransferListener) { super.prepareSourceInternal(mediaTransferListener); - prepareChildSource(null, source); + prepareChildSource(0, source); } + /** + * When any child {@link MediaSource} is prepared, the refreshed {@link Timeline} can + * be listened to here. But since {@link LoadedMediaSource} has only a single child source, + * this method is called only once until {@link #releaseSourceInternal()} is called. + *

+ * On refresh, the {@link CompositeMediaSource} delegate will be notified with the + * new {@link Timeline}, otherwise {@link #createPeriod(MediaPeriodId, Allocator, long)} + * will not be called and playback may be stalled. + * + * @param id The unique id used to prepare the child source. + * @param mediaSource The child source whose source info has been refreshed. + * @param timeline The new timeline of the child source. + */ @Override - protected void onChildSourceInfoRefreshed(final Void id, + protected void onChildSourceInfoRefreshed(final Integer id, final MediaSource mediaSource, final Timeline timeline) { refreshSourceInfo(timeline); diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java index 6b3f91eb3..92d4403c8 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java @@ -18,9 +18,7 @@ final class PlaceholderMediaSource private static final MediaItem MEDIA_ITEM = PlaceholderTag.EMPTY.withExtras(COPY).asMediaItem(); private PlaceholderMediaSource() { } - /** - * Returns the {@link MediaItem} whose media is provided by the source. - */ + @Override public MediaItem getMediaItem() { return MEDIA_ITEM; @@ -30,7 +28,7 @@ final class PlaceholderMediaSource protected void onChildSourceInfoRefreshed(final Void id, final MediaSource mediaSource, final Timeline timeline) { - /* Do nothing, no timeline updates will stall playback */ + /* Do nothing, no timeline updates or error will stall playback */ } @Override From d289dc8a5331bfe941f61cbafa40c3d5328662b1 Mon Sep 17 00:00:00 2001 From: karyogamy Date: Sat, 26 Mar 2022 20:17:52 -0400 Subject: [PATCH 4/5] updated: onPlayerError to not catch unspecified source errors so notifications are created. updated: Throwable usage to Exceptions. updated: minor styles and documentations. --- .../org/schabi/newpipe/player/Player.java | 61 ++++++++++--------- .../player/mediaitem/ExceptionTag.java | 8 +-- .../player/mediaitem/MediaItemTag.java | 2 +- .../player/mediaitem/PlaceholderTag.java | 2 +- .../player/mediaitem/StreamInfoTag.java | 2 +- .../player/mediasource/FailedMediaSource.java | 6 +- .../player/playback/MediaSourceManager.java | 3 +- 7 files changed, 44 insertions(+), 40 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java index ea0cab5e6..ba4c4f237 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -1961,13 +1961,12 @@ public final class Player implements final boolean showPrev = playQueue.getIndex() != 0; final boolean showNext = playQueue.getIndex() + 1 != playQueue.getStreams().size(); final boolean showQueue = playQueue.getStreams().size() > 1 && !popupPlayerSelected(); - boolean showSegment = false; - showSegment = /*only when stream has segment and playing in fullscreen player*/ - !popupPlayerSelected() - && !getCurrentStreamInfo() - .map(StreamInfo::getStreamSegments) - .map(List::isEmpty) - .orElse(/*no stream info=*/true); + /* only when stream has segments and is not playing in popup player */ + final boolean showSegment = !popupPlayerSelected() + && !getCurrentStreamInfo() + .map(StreamInfo::getStreamSegments) + .map(List::isEmpty) + .orElse(/*no stream info=*/true); binding.playPreviousButton.setVisibility(showPrev ? View.VISIBLE : View.INVISIBLE); binding.playPreviousButton.setAlpha(showPrev ? 1.0f : 0.0f); @@ -2014,7 +2013,7 @@ public final class Player implements + "playWhenReady = [" + playWhenReady + "], " + "reason = [" + reason + "]"); } - final int playbackState = simpleExoPlayer == null + final int playbackState = exoPlayerIsNull() ? com.google.android.exoplayer2.Player.STATE_IDLE : simpleExoPlayer.getPlaybackState(); updatePlaybackState(playWhenReady, playbackState); @@ -2026,8 +2025,7 @@ public final class Player implements Log.d(TAG, "ExoPlayer - onPlaybackStateChanged() called with: " + "playbackState = [" + playbackState + "]"); } - final boolean playWhenReady = simpleExoPlayer != null && simpleExoPlayer.getPlayWhenReady(); - updatePlaybackState(playWhenReady, playbackState); + updatePlaybackState(getPlayWhenReady(), playbackState); } private void updatePlaybackState(final boolean playWhenReady, final int playbackState) { @@ -2486,6 +2484,19 @@ public final class Player implements //////////////////////////////////////////////////////////////////////////*/ //region ExoPlayer listeners (that didn't fit in other categories) + /** + *

Listens for event or state changes on ExoPlayer. When any event happens, we check for + * changes in the currently-playing metadata and update the encapsulating + * {@link Player}. Downstream listeners are also informed. + * + *

When the renewed metadata contains any error, it is reported as a notification. + * This is done because not all source resolution errors are {@link PlaybackException}, which + * are also captured by {@link ExoPlayer} and stops the playback. + * + * @param player The {@link com.google.android.exoplayer2.Player} whose state changed. + * @param events The {@link com.google.android.exoplayer2.Player.Events} that has triggered + * the player state changes. + **/ @Override public void onEvents(@NonNull final com.google.android.exoplayer2.Player player, @NonNull final com.google.android.exoplayer2.Player.Events events) { @@ -2602,11 +2613,12 @@ public final class Player implements *

  • {@link PlaybackException#ERROR_CODE_BEHIND_LIVE_WINDOW BEHIND_LIVE_WINDOW}: * If the playback on livestreams are lagged too far behind the current playable * window. Then we seek to the latest timestamp and restart the playback. + * This error is catchable. *
  • *
  • From {@link PlaybackException#ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE BAD_IO} to * {@link PlaybackException#ERROR_CODE_PARSING_MANIFEST_UNSUPPORTED UNSUPPORTED_FORMATS}: * If the stream source is validated by the extractor but not recognized by the player, - * then we can try to recover playback by signal an error on the {@link PlayQueue}.
  • + * then we can try to recover playback by signalling an error on the {@link PlayQueue}. *
  • For {@link PlaybackException#ERROR_CODE_TIMEOUT PLAYER_TIMEOUT}, * {@link PlaybackException#ERROR_CODE_IO_UNSPECIFIED MEDIA_SOURCE_RESOLVER_TIMEOUT} and * {@link PlaybackException#ERROR_CODE_IO_NETWORK_CONNECTION_FAILED NO_NETWORK}: @@ -2617,8 +2629,8 @@ public final class Player implements * We terminate the playback.
  • *
  • For any other unspecified issue internal: We set a recovery and try to restart * the playback.
  • - * In the case of decoder/renderer or unspecified errors, the player will create a - * notification so the users are aware. + * For any error above that is not explicitly catchable, the player will + * create a notification so users are aware. * * @see com.google.android.exoplayer2.Player.Listener#onPlayerError(PlaybackException) * */ @@ -2627,7 +2639,6 @@ public final class Player implements public void onPlayerError(@NonNull final PlaybackException error) { Log.e(TAG, "ExoPlayer - onPlayerError() called with:", error); - setRecovery(); saveStreamProgressState(); boolean isCatchableException = false; @@ -2652,7 +2663,6 @@ public final class Player implements case ERROR_CODE_PARSING_MANIFEST_UNSUPPORTED: // Source errors, signal on playQueue and move on: if (!exoPlayerIsNull() && playQueue != null) { - isCatchableException = true; playQueue.error(); } break; @@ -2660,11 +2670,6 @@ public final class Player implements case ERROR_CODE_IO_UNSPECIFIED: case ERROR_CODE_IO_NETWORK_CONNECTION_FAILED: case ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT: - // Don't create notification on timeout/networking errors: - isCatchableException = true; - setRecovery(); - reloadPlayQueueManager(); - break; case ERROR_CODE_UNSPECIFIED: // Reload playback on unexpected errors: setRecovery(); @@ -3010,10 +3015,9 @@ public final class Player implements } public void saveStreamProgressStateCompleted() { - getCurrentStreamInfo().ifPresent(info -> { - // current stream has ended, so the progress is its duration (+1 to overcome rounding) - saveStreamProgressState((info.getDuration() + 1) * 1000); - }); + // current stream has ended, so the progress is its duration (+1 to overcome rounding) + getCurrentStreamInfo().ifPresent(info -> + saveStreamProgressState((info.getDuration() + 1) * 1000)); } //endregion @@ -3414,7 +3418,8 @@ public final class Player implements case VIDEO_STREAM: if (currentMetadata == null || !currentMetadata.getMaybeQuality().isPresent() - || info.getVideoStreams().size() + info.getVideoOnlyStreams().size() == 0) { + || (info.getVideoStreams().isEmpty() + && info.getVideoOnlyStreams().isEmpty())) { break; } @@ -3684,10 +3689,8 @@ public final class Player implements } // Normalize mismatching language strings - final List preferredLanguages = - trackSelector.getParameters().preferredTextLanguages; - final String preferredLanguage = - preferredLanguages.isEmpty() ? null : preferredLanguages.get(0); + final String preferredLanguage = trackSelector.getParameters() + .preferredTextLanguages.stream().findFirst().orElse(null); // Build UI buildCaptionMenu(availableLanguages); if (trackSelector.getParameters().getRendererDisabled(textRenderer) diff --git a/app/src/main/java/org/schabi/newpipe/player/mediaitem/ExceptionTag.java b/app/src/main/java/org/schabi/newpipe/player/mediaitem/ExceptionTag.java index 7e28eda20..ebedf8c71 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediaitem/ExceptionTag.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediaitem/ExceptionTag.java @@ -24,12 +24,12 @@ public final class ExceptionTag implements MediaItemTag { @NonNull private final PlayQueueItem item; @NonNull - private final List errors; + private final List errors; @Nullable private final Object extras; private ExceptionTag(@NonNull final PlayQueueItem item, - @NonNull final List errors, + @NonNull final List errors, @Nullable final Object extras) { this.item = item; this.errors = errors; @@ -37,13 +37,13 @@ public final class ExceptionTag implements MediaItemTag { } public static ExceptionTag of(@NonNull final PlayQueueItem playQueueItem, - @NonNull final List errors) { + @NonNull final List errors) { return new ExceptionTag(playQueueItem, errors, null); } @NonNull @Override - public List getErrors() { + public List getErrors() { return errors; } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediaitem/MediaItemTag.java b/app/src/main/java/org/schabi/newpipe/player/mediaitem/MediaItemTag.java index 8dbd55280..f84b0383a 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediaitem/MediaItemTag.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediaitem/MediaItemTag.java @@ -26,7 +26,7 @@ import androidx.annotation.Nullable; **/ public interface MediaItemTag { - List getErrors(); + List getErrors(); int getServiceId(); diff --git a/app/src/main/java/org/schabi/newpipe/player/mediaitem/PlaceholderTag.java b/app/src/main/java/org/schabi/newpipe/player/mediaitem/PlaceholderTag.java index fe7aa9d92..cce4e9f17 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediaitem/PlaceholderTag.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediaitem/PlaceholderTag.java @@ -29,7 +29,7 @@ public final class PlaceholderTag implements MediaItemTag { @NonNull @Override - public List getErrors() { + public List getErrors() { return Collections.emptyList(); } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediaitem/StreamInfoTag.java b/app/src/main/java/org/schabi/newpipe/player/mediaitem/StreamInfoTag.java index 6a942840d..4095f2bc8 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediaitem/StreamInfoTag.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediaitem/StreamInfoTag.java @@ -47,7 +47,7 @@ public final class StreamInfoTag implements MediaItemTag { } @Override - public List getErrors() { + public List getErrors() { return Collections.emptyList(); } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java index 299ae845d..fa52ab0ee 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java @@ -36,7 +36,7 @@ public class FailedMediaSource extends BaseMediaSource implements ManagedMediaSo private final String TAG = "FailedMediaSource@" + Integer.toHexString(hashCode()); private final PlayQueueItem playQueueItem; - private final Throwable error; + private final Exception error; private final long retryTimestamp; private final MediaItem mediaItem; /** @@ -51,7 +51,7 @@ public class FailedMediaSource extends BaseMediaSource implements ManagedMediaSo * @param retryTimestamp epoch timestamp when this MediaSource can be refreshed */ public FailedMediaSource(@NonNull final PlayQueueItem playQueueItem, - @NonNull final Throwable error, + @NonNull final Exception error, final long retryTimestamp) { this.playQueueItem = playQueueItem; this.error = error; @@ -68,7 +68,7 @@ public class FailedMediaSource extends BaseMediaSource implements ManagedMediaSo } public static FailedMediaSource of(@NonNull final PlayQueueItem playQueueItem, - @NonNull final Throwable error, + @NonNull final Exception error, final long retryWaitMillis) { return new FailedMediaSource(playQueueItem, error, System.currentTimeMillis() + retryWaitMillis); diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java index d4ed973aa..b4e9a15ab 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java @@ -441,7 +441,8 @@ public class MediaSourceManager { if (throwable instanceof ExtractionException) { return FailedMediaSource.of(stream, new StreamInfoLoadException(throwable)); } - return FailedMediaSource.of(stream, throwable, /*immediatelyRetryable=*/0L); + return FailedMediaSource + .of(stream, new Exception(throwable), /*immediatelyRetryable=*/0L); }); } From a00bc95acccd5c4c253726ecb4decb21d8cd228f Mon Sep 17 00:00:00 2001 From: karyogamy Date: Sun, 27 Mar 2022 13:24:37 -0400 Subject: [PATCH 5/5] updated: source loading error for FailedMediaSource to wait for 3 seconds before allowing retry. updated: minor style fixes. --- app/src/main/java/org/schabi/newpipe/player/Player.java | 7 +++++-- .../newpipe/player/mediasource/FailedMediaSource.java | 2 +- .../schabi/newpipe/player/playback/MediaSourceManager.java | 6 ++++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java index ba4c4f237..c4205124c 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -2487,11 +2487,11 @@ public final class Player implements /** *

    Listens for event or state changes on ExoPlayer. When any event happens, we check for * changes in the currently-playing metadata and update the encapsulating - * {@link Player}. Downstream listeners are also informed. + * {@link Player}. Downstream listeners are also informed.

    * *

    When the renewed metadata contains any error, it is reported as a notification. * This is done because not all source resolution errors are {@link PlaybackException}, which - * are also captured by {@link ExoPlayer} and stops the playback. + * are also captured by {@link ExoPlayer} and stops the playback.

    * * @param player The {@link com.google.android.exoplayer2.Player} whose state changed. * @param events The {@link com.google.android.exoplayer2.Player.Events} that has triggered @@ -2634,6 +2634,9 @@ public final class Player implements * * @see com.google.android.exoplayer2.Player.Listener#onPlayerError(PlaybackException) * */ + // Any error code not explicitly covered here are either unrelated to NewPipe use case + // (e.g. DRM) or not recoverable (e.g. Decoder error). In both cases, the player should + // shutdown. @SuppressLint("SwitchIntDef") @Override public void onPlayerError(@NonNull final PlaybackException error) { diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java index fa52ab0ee..8aad356d0 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java @@ -78,7 +78,7 @@ public class FailedMediaSource extends BaseMediaSource implements ManagedMediaSo return playQueueItem; } - public Throwable getError() { + public Exception getError() { return error; } diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java index b4e9a15ab..9b13bb3d7 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java @@ -441,8 +441,10 @@ public class MediaSourceManager { if (throwable instanceof ExtractionException) { return FailedMediaSource.of(stream, new StreamInfoLoadException(throwable)); } - return FailedMediaSource - .of(stream, new Exception(throwable), /*immediatelyRetryable=*/0L); + // Non-source related error expected here (e.g. network), + // should allow retry shortly after the error. + return FailedMediaSource.of(stream, new Exception(throwable), + /*allowRetryIn=*/TimeUnit.MILLISECONDS.convert(3, TimeUnit.SECONDS)); }); }