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); } }