From 4e459b33839e428c26c5730d629ba07356ca88e3 Mon Sep 17 00:00:00 2001
From: karyogamy
Date: Sun, 13 Mar 2022 00:22:47 -0500
Subject: [PATCH 1/5] updated: ExoPlayer to 2.17.1. added: MediaItemTag for
ManagedMediaSources. added: silent track for FailedMediaSource. added:
keyframe fast forward at initial playback buffer. added: error notification
on silently skipped streams.
---
app/build.gradle | 2 +-
.../fragments/detail/VideoDetailFragment.java | 7 +-
.../detail/VideoDetailPlayerCrasher.java | 17 +-
.../org/schabi/newpipe/player/Player.java | 450 +++++++++---------
.../event/PlayerServiceEventListener.java | 4 +-
.../newpipe/player/helper/AudioReactor.java | 6 +-
.../newpipe/player/helper/CacheFactory.java | 18 +-
.../player/helper/MediaSessionManager.java | 2 -
.../player/helper/PlayerDataSource.java | 15 +-
.../newpipe/player/helper/PlayerHolder.java | 7 +-
.../player/mediaitem/ExceptionTag.java | 99 ++++
.../player/mediaitem/MediaItemTag.java | 113 +++++
.../player/mediaitem/PlaceholderTag.java | 89 ++++
.../player/mediaitem/StreamInfoTag.java | 105 ++++
.../mediasession/PlayQueueNavigator.java | 20 +-
.../PlayQueuePlaybackController.java | 23 -
.../player/mediasource/FailedMediaSource.java | 105 ++--
.../player/mediasource/LoadedMediaSource.java | 90 +---
.../mediasource/ManagedMediaSource.java | 7 -
.../ManagedMediaSourcePlaylist.java | 18 +-
.../mediasource/PlaceholderMediaSource.java | 33 +-
.../player/playback/CustomTrackSelector.java | 92 ----
.../player/playback/MediaSourceManager.java | 52 +-
.../player/playback/PlaybackListener.java | 5 +-
.../playback/SurfaceHolderCallback.java | 6 +-
.../resolver/AudioPlaybackResolver.java | 4 +-
.../player/resolver/MediaSourceTag.java | 53 ---
.../player/resolver/PlaybackResolver.java | 27 +-
.../resolver/VideoPlaybackResolver.java | 22 +-
29 files changed, 885 insertions(+), 606 deletions(-)
create mode 100644 app/src/main/java/org/schabi/newpipe/player/mediaitem/ExceptionTag.java
create mode 100644 app/src/main/java/org/schabi/newpipe/player/mediaitem/MediaItemTag.java
create mode 100644 app/src/main/java/org/schabi/newpipe/player/mediaitem/PlaceholderTag.java
create mode 100644 app/src/main/java/org/schabi/newpipe/player/mediaitem/StreamInfoTag.java
delete mode 100644 app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueuePlaybackController.java
delete mode 100644 app/src/main/java/org/schabi/newpipe/player/playback/CustomTrackSelector.java
delete mode 100644 app/src/main/java/org/schabi/newpipe/player/resolver/MediaSourceTag.java
diff --git a/app/build.gradle b/app/build.gradle
index 0299c0fd598..929a7820ddc 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 0af5ec99e57..c57942aa56f 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.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 void onMetadataUpdate(final StreamInfo info, final PlayQueue queue) {
}
@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 9309a8a4976..ae704e88c99 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 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.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 @@ private static Map> getExceptionTypes() {
exceptionTypes.put(
"Source",
() -> ExoPlaybackException.createForSource(
- new IOException(defaultMsg)
+ new IOException(defaultMsg),
+ ERROR_CODE_BEHIND_LIVE_WINDOW
)
);
exceptionTypes.put(
@@ -61,13 +67,16 @@ private static Map> getExceptionTypes() {
"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 static void onCrashThePlayer(
/**
* 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 1051f678f3f..2305eb9d073 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 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.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.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.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.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 Player(@NonNull final MainPlayer service) {
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 @@ private void initPlayer(final boolean playOnReady) {
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 float getPlaybackPitch() {
}
public boolean getPlaybackSkipSilence() {
- return !exoPlayerIsNull() && simpleExoPlayer.getAudioComponent() != null
- && simpleExoPlayer.getAudioComponent().getSkipSilenceEnabled();
+ return !exoPlayerIsNull() && simpleExoPlayer.getSkipSilenceEnabled();
}
public PlaybackParameters getPlaybackParameters() {
@@ -1669,9 +1684,7 @@ public void setPlaybackParameters(final float speed, final float pitch,
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 @@ private void showOrHideButtons() {
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 @@ private void hideSystemUIIfNeeded() {
// 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 void onPlayerStateChanged(final boolean playWhenReady, final int playback
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 void onPlayerStateChanged(final boolean playWhenReady, final int playback
}
break;
case com.google.android.exoplayer2.Player.STATE_READY: //3
- maybeUpdateCurrentMetadata();
- maybeCorrectSeekPosition();
if (!isPrepared) {
isPrepared = true;
onPrepared(playWhenReady);
@@ -2037,18 +2071,11 @@ public void onPlayerStateChanged(final boolean playWhenReady, final int playback
@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 @@ private void setMuteButton(@NonNull final ImageButton button, final boolean isMu
//////////////////////////////////////////////////////////////////////////*/
//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 void onPositionDiscontinuity(@NonNull final PositionInfo oldPosition,
@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 void onPositionDiscontinuity(@NonNull final PositionInfo oldPosition,
}
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 void onCues(@NonNull final List cues) {
//region Errors
/**
* Process exceptions produced by {@link com.google.android.exoplayer2.ExoPlayer ExoPlayer}.
- * There are multiple types of errors:
- *
- * - {@link ExoPlaybackException#TYPE_SOURCE TYPE_SOURCE}
- * - {@link ExoPlaybackException#TYPE_UNEXPECTED TYPE_UNEXPECTED}:
- * If a runtime error occurred, then we can try to recover it by restarting the playback
- * after setting the timestamp recovery.
- * - {@link ExoPlaybackException#TYPE_RENDERER TYPE_RENDERER}:
- * If the renderer failed, treat the error as unrecoverable.
- *
*
- * @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 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 ExoPlaybackException.TYPE_UNEXPECTED:
+ 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 boolean isLiveEdge() {
}
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 boolean isLiveEdge() {
}
@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 void onPlaybackSynchronize(@NonNull final PlayQueueItem item) {
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 void onPlaybackSynchronize(@NonNull final PlayQueueItem item) {
+ "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 void onPlaybackSynchronize(@NonNull final PlayQueueItem item) {
}
}
- 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 void fastRewind() {
//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 @@ private void saveStreamProgressState(final long progressMillis) {
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 void saveStreamProgressState() {
}
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 void saveStreamProgressStateCompleted() {
//////////////////////////////////////////////////////////////////////////*/
//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 @@ private void onMetadataChanged(@NonNull final MediaSourceTag tag) {
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 @@ private void onMetadataChanged(@NonNull final MediaSourceTag tag) {
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 @@ private void onMetadataChanged(@NonNull final MediaSourceTag tag) {
}
}
- 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 @@ private String getVideoUrlAtCurrentTime() {
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 @@ private String getVideoUrlAtCurrentTime() {
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 Bitmap getThumbnail() {
//////////////////////////////////////////////////////////////////////////*/
//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 @@ private void buildSegments() {
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 @@ private StreamSegmentAdapter.StreamSegmentListener getStreamSegmentListener() {
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 VideoStream getSelectedVideoStream() {
}
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 @@ private void updateStreamRelatedViews() {
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 @@ private void buildCaptionMenu(@NonNull final List availableLanguages) {
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 @@ private void buildCaptionMenu(@NonNull final List availableLanguages) {
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 @@ private void onTextTracksChanged() {
}
// 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 @@ private void onPlayWithKodiClicked() {
}
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 @@ private void notifyQueueUpdateToListeners() {
}
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 @@ private void useVideoSource(final boolean videoEnabled) {
// 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 @@ && isNullOrEmpty(streamInfo.getAudioStreams()))) {
//////////////////////////////////////////////////////////////////////////*/
//region Getters
+ private Optional getCurrentStreamInfo() {
+ return Optional.ofNullable(currentMetadata).flatMap(MediaItemTag::getMaybeStreamInfo);
+ }
+
public int getCurrentState() {
return currentState;
}
@@ -4322,8 +4321,7 @@ public boolean exoPlayerIsNull() {
}
public boolean isStopped() {
- return exoPlayerIsNull()
- || simpleExoPlayer.getPlaybackState() == SimpleExoPlayer.STATE_IDLE;
+ return exoPlayerIsNull() || simpleExoPlayer.getPlaybackState() == ExoPlayer.STATE_IDLE;
}
public boolean isPlaying() {
@@ -4340,7 +4338,7 @@ private boolean isLoading() {
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 @@ private void setupVideoSurface() {
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 f8d03087e0d..359eab8b28e 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 087a3bc76b6..a05990816de 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.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 bcab92787a7..98e04d4661d 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 @@
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 java.io.File;
+import androidx.annotation.NonNull;
+
/* package-private */ class CacheFactory implements DataSource.Factory {
private static final String TAG = "CacheFactory";
@@ -25,7 +25,7 @@
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 @@ private CacheFactory(@NonNull final Context context,
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 @@ private CacheFactory(@NonNull final Context context,
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 @@ private CacheFactory(@NonNull final Context context,
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 cd04bc2ebcb..c12ba754ad4 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 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 MediaSessionManager(@NonNull final Context context,
.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 d7a9ffc3d24..405f6fd37b7 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 @@
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.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 06a2e52ab3e..4c09ed3c19a 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 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 void onMoreOptionsLongClicked() {
}
@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 00000000000..2deffcf65b6
--- /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 00000000000..872a10a578d
--- /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 00000000000..c4998e9afaa
--- /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 00000000000..93cf081f974
--- /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 62664c8270d..92cd425c5fb 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 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 long getSupportedQueueNavigatorActions(@Nullable final Player player) {
}
@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 long getActiveQueueItemId(@Nullable final Player player) {
}
@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 @@ private void publishFloatingQueueWindow() {
}
@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 8bfbcde6bb9..00000000000
--- 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 7594f3a1600..75fbbe433f9 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 @@
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;
-
- public FailedMediaSource(@NonNull final PlayQueueItem playQueueItem,
- @NonNull final FailedMediaSourceException error,
- final long retryTimestamp) {
- this.playQueueItem = playQueueItem;
- this.error = error;
- this.retryTimestamp = 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 always be propagated to ExoPlayer.
*
- * @param playQueueItem play queue item
- * @param error exception that was the reason to fail
+ * 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 = Long.MAX_VALUE;
+ 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();
+ }
+
+ 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,30 +88,45 @@ private boolean canRetry() {
*/
@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 MediaPeriod createPeriod(final MediaPeriodId id, final Allocator allocator,
- final long startPositionUs) {
- return null;
+ public void maybeThrowSourceInfoRefreshError() throws IOException {
+ if (!(error instanceof FailedMediaSourceException)) {
+ throw new IOException(error);
+ }
+ super.maybeThrowSourceInfoRefreshError();
}
@Override
- public void releasePeriod(final MediaPeriod mediaPeriod) { }
+ protected void onChildSourceInfoRefreshed(final Void id,
+ final MediaSource mediaSource,
+ final Timeline timeline) {
+ refreshSourceInfo(timeline);
+ }
+
@Override
- protected void prepareSourceInternal(@Nullable final TransferListener mediaTransferListener) {
- Log.e(TAG, "Loading failed source: ", error);
+ public MediaPeriod createPeriod(final MediaPeriodId id, final Allocator allocator,
+ final long startPositionUs) {
+ return source.createPeriod(id, allocator, startPositionUs);
}
@Override
- protected void releaseSourceInternal() { }
+ public void releasePeriod(final MediaPeriod mediaPeriod) {
+ source.releasePeriod(mediaPeriod);
+ }
@Override
public boolean shouldBeReplacedWith(@NonNull final PlayQueueItem newIdentity,
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 746a9758156..193b90271ac 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 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.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.MediaSourceEventListener;
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 java.io.IOException;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
-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 @@ private boolean isExpired() {
}
@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 void releasePeriod(final MediaPeriod mediaPeriod) {
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 21fddbe861b..9d6b948937b 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 ff0cf21fa4b..4c03807672e 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 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 int size() {
*/
@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 ConcatenatingMediaSource getParentMediaSource() {
* @see #append(ManagedMediaSource)
*/
public synchronized void expand() {
- append(new PlaceholderMediaSource());
+ append(PlaceholderMediaSource.COPY);
}
/**
@@ -115,10 +123,10 @@ public synchronized void move(final int source, final int target) {
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 1cd8556270b..6b3f91eb32c 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 MediaPeriod createPeriod(final MediaPeriodId id, final Allocator allocato
@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 389be70628e..00000000000
--- 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 f3049d11dca..d4ed973aa59 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 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 void dispose() {
//////////////////////////////////////////////////////////////////////////*/
private Subscriber getReactor() {
- return new Subscriber() {
+ return new Subscriber<>() {
@Override
public void onSubscribe(@NonNull final Subscription d) {
playQueueReactor.cancel();
@@ -209,10 +210,12 @@ public void onNext(@NonNull final PlayQueueEvent playQueueMessage) {
}
@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 @@ private boolean isPlaybackReady() {
}
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 @@ private void maybeBlock() {
isBlocked.set(true);
}
- private void maybeUnblock() {
+ private boolean maybeUnblock() {
if (DEBUG) {
Log.d(TAG, "maybeUnblock() called.");
}
@@ -323,14 +326,17 @@ private void maybeUnblock() {
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 @@ private void maybeSync() {
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 @@ private void maybeLoadItem(@NonNull final PlayQueueItem item) {
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 @@ private boolean isCorrectionNeeded(@NonNull final PlayQueueItem item) {
/**
* 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 811f82b3b71..73760700155 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 0814092fa53..5d67e6967ef 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 @@
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 @@
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 29be402c53f..9bded9331c7 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.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 MediaSource resolve(@NonNull final StreamInfo info) {
}
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 360e92e7fcc..00000000000
--- 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 cfe9dbb62b2..90b38ed51da 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 @@
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 @@ default MediaSource maybeBuildLiveMediaSource(@NonNull final PlayerDataSource da
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 @@ default MediaSource maybeBuildLiveMediaSource(@NonNull final PlayerDataSource da
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 @@ default MediaSource buildLiveMediaSource(@NonNull final PlayerDataSource dataSou
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 @@ default MediaSource buildMediaSource(@NonNull final PlayerDataSource dataSource,
@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 11949f55dec..565f0b23e17 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.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 MediaSource resolve(@NonNull final StreamInfo info) {
} 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 MediaSource resolve(@NonNull final StreamInfo info) {
if (mimeType == null) {
continue;
}
- final MediaSource textSource = dataSource.getSampleMediaSourceFactory()
- .createMediaSource(
- new MediaItem.Subtitle(Uri.parse(subtitle.getUrl()),
- mimeType,
- PlayerHelper.captionLanguageOf(context, subtitle)),
- TIME_UNSET);
+ final MediaItem.SubtitleConfiguration textMediaItem =
+ new MediaItem.SubtitleConfiguration.Builder(Uri.parse(subtitle.getUrl()))
+ .setMimeType(mimeType)
+ .setLanguage(PlayerHelper.captionLanguageOf(context, subtitle))
+ .build();
+ final MediaSource textSource = dataSource
+ .getSampleMediaSourceFactory()
+ .createMediaSource(textMediaItem, TIME_UNSET);
mediaSources.add(textSource);
}
}
From 69646e5b5d5adae43abafcfa47b2aa587665ddb7 Mon Sep 17 00:00:00 2001
From: karyogamy
Date: Wed, 16 Mar 2022 20:47:29 -0400
Subject: [PATCH 2/5] added: documentations to MediaItemTags and Player. fixed:
checkStyle failures.
---
.../org/schabi/newpipe/player/Player.java | 53 ++++++++++++-------
.../player/mediaitem/ExceptionTag.java | 10 ++++
.../player/mediaitem/MediaItemTag.java | 8 +++
.../player/mediaitem/PlaceholderTag.java | 11 +++-
.../player/mediaitem/StreamInfoTag.java | 8 +++
.../player/mediasource/FailedMediaSource.java | 12 ++++-
6 files changed, 79 insertions(+), 23 deletions(-)
diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java
index 2305eb9d073..355cd407969 100644
--- a/app/src/main/java/org/schabi/newpipe/player/Player.java
+++ b/app/src/main/java/org/schabi/newpipe/player/Player.java
@@ -133,7 +133,6 @@
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player.PositionInfo;
import com.google.android.exoplayer2.RenderersFactory;
-import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.TracksInfo;
import com.google.android.exoplayer2.source.MediaSource;
@@ -2487,6 +2486,7 @@ private void setMuteButton(@NonNull final ImageButton button, final boolean isMu
//////////////////////////////////////////////////////////////////////////*/
//region ExoPlayer listeners (that didn't fit in other categories)
+ @Override
public void onEvents(@NonNull final com.google.android.exoplayer2.Player player,
@NonNull final com.google.android.exoplayer2.Player.Events events) {
Listener.super.onEvents(player, events);
@@ -2546,14 +2546,6 @@ public void onPositionDiscontinuity(@NonNull final PositionInfo oldPosition,
return;
}
- if (newPosition.contentPositionMs == 0 &&
- simpleExoPlayer.getTotalBufferedDuration() < 500L) {
- Log.d(TAG, "Playback - skipping to initial keyframe.");
- simpleExoPlayer.setSeekParameters(SeekParameters.CLOSEST_SYNC);
- simpleExoPlayer.seekTo(1L);
- simpleExoPlayer.setSeekParameters(PlayerHelper.getSeekParameters(context));
- }
-
// Refresh the playback if there is a transition to the next video
final int newIndex = newPosition.mediaItemIndex;
switch (discontinuityReason) {
@@ -2605,7 +2597,29 @@ public void onCues(@NonNull final List cues) {
//region Errors
/**
* Process exceptions produced by {@link com.google.android.exoplayer2.ExoPlayer ExoPlayer}.
- *
+ * There are multiple types of errors:
+ *
+ * - {@link PlaybackException#ERROR_CODE_BEHIND_LIVE_WINDOW BEHIND_LIVE_WINDOW}:
+ * If the playback on livestreams are lagged too far behind the current playable
+ * window. Then we seek to the latest timestamp and restart the playback.
+ *
+ * - From {@link PlaybackException#ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE BAD_IO} to
+ * {@link PlaybackException#ERROR_CODE_PARSING_MANIFEST_UNSUPPORTED UNSUPPORTED_FORMATS}:
+ * If the stream source is validated by the extractor but not recognized by the player,
+ * then we can try to recover playback by signal an error on the {@link PlayQueue}.
+ * - For {@link PlaybackException#ERROR_CODE_TIMEOUT PLAYER_TIMEOUT},
+ * {@link PlaybackException#ERROR_CODE_IO_UNSPECIFIED MEDIA_SOURCE_RESOLVER_TIMEOUT} and
+ * {@link PlaybackException#ERROR_CODE_IO_NETWORK_CONNECTION_FAILED NO_NETWORK}:
+ * We can keep set the recovery record and keep to player at the current state until
+ * it is ready to play by restarting the {@link MediaSourceManager}.
+ * - On any ExoPlayer specific issue internal to its device interaction, such as
+ * {@link PlaybackException#ERROR_CODE_DECODER_INIT_FAILED DECODER_ERROR}:
+ * We terminate the playback.
+ * - For any other unspecified issue internal: We set a recovery and try to restart
+ * the playback.
+ * In the case of decoder/renderer or unspecified errors, the player will create a
+ * notification so the users are aware.
+ *
* @see com.google.android.exoplayer2.Player.Listener#onPlayerError(PlaybackException)
* */
@SuppressLint("SwitchIntDef")
@@ -2648,6 +2662,9 @@ public void onPlayerError(@NonNull final PlaybackException error) {
case ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT:
// Don't create notification on timeout/networking errors:
isCatchableException = true;
+ setRecovery();
+ reloadPlayQueueManager();
+ break;
case ERROR_CODE_UNSPECIFIED:
// Reload playback on unexpected errors:
setRecovery();
@@ -2749,7 +2766,6 @@ public void onPlaybackSynchronize(@NonNull final PlayQueueItem item, final boole
return;
}
- final boolean onPlaybackInitial = currentItem == null;
final boolean hasPlayQueueItemChanged = currentItem != item;
final int currentPlayQueueIndex = playQueue.indexOf(item);
@@ -2953,10 +2969,8 @@ public void fastRewind() {
//region StreamInfo history: views and progress
private void registerStreamViewed() {
- getCurrentStreamInfo().ifPresent(info -> {
- databaseUpdateDisposable
- .add(recordManager.onViewed(info).onErrorComplete().subscribe());
- });
+ getCurrentStreamInfo().ifPresent(info -> databaseUpdateDisposable
+ .add(recordManager.onViewed(info).onErrorComplete().subscribe()));
}
private void saveStreamProgressState(final long progressMillis) {
@@ -3134,7 +3148,7 @@ public void selectQueueItem(final PlayQueueItem item) {
return;
}
- if (playQueue.getIndex() == index && simpleExoPlayer.getCurrentWindowIndex() == index) {
+ if (playQueue.getIndex() == index && simpleExoPlayer.getCurrentMediaItemIndex() == index) {
seekToDefault();
} else {
saveStreamProgressState();
@@ -3880,9 +3894,10 @@ private void onPlayWithKodiClicked() {
}
private void onOpenInBrowserClicked() {
- getCurrentStreamInfo().map(Info::getOriginalUrl).ifPresent(originalUrl -> {
- ShareUtils.openUrlInBrowser(Objects.requireNonNull(getParentActivity()), originalUrl);
- });
+ getCurrentStreamInfo()
+ .map(Info::getOriginalUrl)
+ .ifPresent(originalUrl -> ShareUtils.openUrlInBrowser(
+ Objects.requireNonNull(getParentActivity()), originalUrl));
}
//endregion
diff --git a/app/src/main/java/org/schabi/newpipe/player/mediaitem/ExceptionTag.java b/app/src/main/java/org/schabi/newpipe/player/mediaitem/ExceptionTag.java
index 2deffcf65b6..3e41262d660 100644
--- a/app/src/main/java/org/schabi/newpipe/player/mediaitem/ExceptionTag.java
+++ b/app/src/main/java/org/schabi/newpipe/player/mediaitem/ExceptionTag.java
@@ -10,6 +10,16 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+/**
+ * This {@link MediaItemTag} object is designed to contain metadata for a stream
+ * that has failed to load. It supplies metadata from an underlying
+ * {@link PlayQueueItem}, which is used by the internal players to resolve actual
+ * playback info.
+ *
+ * This {@link MediaItemTag} does not contain any {@link StreamInfo} that can be
+ * used to start playback and can be detected by checking {@link ExceptionTag#getErrors()}
+ * when in generic form.
+ **/
public final class ExceptionTag implements MediaItemTag {
@NonNull
private final PlayQueueItem item;
diff --git a/app/src/main/java/org/schabi/newpipe/player/mediaitem/MediaItemTag.java b/app/src/main/java/org/schabi/newpipe/player/mediaitem/MediaItemTag.java
index 872a10a578d..6f9b9e9f245 100644
--- a/app/src/main/java/org/schabi/newpipe/player/mediaitem/MediaItemTag.java
+++ b/app/src/main/java/org/schabi/newpipe/player/mediaitem/MediaItemTag.java
@@ -4,6 +4,7 @@
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.MediaMetadata;
+import com.google.android.exoplayer2.Player;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamType;
@@ -16,6 +17,13 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+/**
+ * Metadata container and accessor used by player internals.
+ *
+ * This interface ensures consistency of fetching metadata on each stream,
+ * which is encapsulated in a {@link MediaItem} and delivered via ExoPlayer's
+ * {@link Player.Listener} on event triggers to the downstream users.
+ **/
public interface MediaItemTag {
List getErrors();
diff --git a/app/src/main/java/org/schabi/newpipe/player/mediaitem/PlaceholderTag.java b/app/src/main/java/org/schabi/newpipe/player/mediaitem/PlaceholderTag.java
index c4998e9afaa..5b53e5660d2 100644
--- a/app/src/main/java/org/schabi/newpipe/player/mediaitem/PlaceholderTag.java
+++ b/app/src/main/java/org/schabi/newpipe/player/mediaitem/PlaceholderTag.java
@@ -2,6 +2,7 @@
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamType;
+import org.schabi.newpipe.util.Constants;
import java.util.Collections;
import java.util.List;
@@ -10,6 +11,12 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+/**
+ * This is a Placeholding {@link MediaItemTag}, designed as a dummy metadata object for
+ * any stream that has not been resolved.
+ *
+ * This object cannot be instantiated and does not hold real metadata of any form.
+ * */
public final class PlaceholderTag implements MediaItemTag {
public static final PlaceholderTag EMPTY = new PlaceholderTag(null);
private static final String UNKNOWN_VALUE_INTERNAL = "Placeholder";
@@ -29,7 +36,7 @@ public List getErrors() {
@Override
public int getServiceId() {
- return -1;
+ return Constants.NO_SERVICE_ID;
}
@Override
@@ -44,7 +51,7 @@ public String getUploaderName() {
@Override
public long getDurationSeconds() {
- return -1;
+ return 0;
}
@Override
diff --git a/app/src/main/java/org/schabi/newpipe/player/mediaitem/StreamInfoTag.java b/app/src/main/java/org/schabi/newpipe/player/mediaitem/StreamInfoTag.java
index 93cf081f974..9ae25e07b35 100644
--- a/app/src/main/java/org/schabi/newpipe/player/mediaitem/StreamInfoTag.java
+++ b/app/src/main/java/org/schabi/newpipe/player/mediaitem/StreamInfoTag.java
@@ -1,5 +1,7 @@
package org.schabi.newpipe.player.mediaitem;
+import com.google.android.exoplayer2.MediaItem;
+
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.extractor.stream.VideoStream;
@@ -11,6 +13,12 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+/**
+ * This {@link MediaItemTag} object contains metadata for a resolved stream
+ * that is ready for playback. This object guarantees the {@link StreamInfo}
+ * is available and may provide the {@link Quality} of video stream used in
+ * the {@link MediaItem}.
+ **/
public final class StreamInfoTag implements MediaItemTag {
@NonNull
private final StreamInfo streamInfo;
diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java
index 75fbbe433f9..d68eb1a1a60 100644
--- a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java
+++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java
@@ -23,7 +23,15 @@
import androidx.annotation.Nullable;
public class FailedMediaSource extends CompositeMediaSource implements ManagedMediaSource {
- private static final long SILENCE_DURATION_US = TimeUnit.SECONDS.toMicros(2);
+ /**
+ * Play 2 seconds of silenced audio when a stream fails to resolve due to a known issue,
+ * such as {@link org.schabi.newpipe.extractor.exceptions.ExtractionException}.
+ *
+ * This silence duration allows user to react and have time to jump to a previous stream,
+ * while still provide a smooth playback experience. A duration lower than 1 second is
+ * not recommended, it may cause ExoPlayer to buffer for a while.
+ * */
+ public static final long SILENCE_DURATION_US = TimeUnit.SECONDS.toMicros(2);
private final String TAG = "FailedMediaSource@" + Integer.toHexString(hashCode());
private final PlayQueueItem playQueueItem;
@@ -32,7 +40,7 @@ public class FailedMediaSource extends CompositeMediaSource implements Man
private final MediaSource source;
private final MediaItem mediaItem;
/**
- * Permanently fail the play queue item associated with this source, with no hope of retrying.
+ * Fail the play queue item associated with this source, with potential future retries.
*
* The error will be propagated if the cause for load exception is unspecified.
* This means the error might be caused by reasons outside of extraction (e.g. no network).
From b81eb35f3d045e9bbaa77e8fcfc13ab1fb43cd09 Mon Sep 17 00:00:00 2001
From: karyogamy
Date: Thu, 17 Mar 2022 22:56:22 -0400
Subject: [PATCH 3/5] added: documentations on lifecycles for FailedMediaSource
and LoadedMediaSource. fixed: onPlaybackSynchronize to rewind when not
playing, which was incorrectly removed in previous commit. fixed: sonar and
checkstyle issues.
---
.../org/schabi/newpipe/player/Player.java | 2 +-
.../player/mediaitem/ExceptionTag.java | 10 --
.../player/mediaitem/MediaItemTag.java | 12 ++-
.../player/mediaitem/PlaceholderTag.java | 11 --
.../player/mediaitem/StreamInfoTag.java | 2 +
.../player/mediasource/FailedMediaSource.java | 101 ++++++++++++------
.../player/mediasource/LoadedMediaSource.java | 39 ++++++-
.../mediasource/PlaceholderMediaSource.java | 6 +-
8 files changed, 121 insertions(+), 62 deletions(-)
diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java
index 355cd407969..ea0cab5e652 100644
--- a/app/src/main/java/org/schabi/newpipe/player/Player.java
+++ b/app/src/main/java/org/schabi/newpipe/player/Player.java
@@ -2791,7 +2791,7 @@ public void onPlaybackSynchronize(@NonNull final PlayQueueItem item, final boole
+ "index=[" + currentPlayQueueIndex + "] with "
+ "playlist length=[" + currentPlaylistSize + "]");
- } else if (wasBlocked || currentPlaylistIndex != currentPlayQueueIndex) {
+ } else if (wasBlocked || currentPlaylistIndex != currentPlayQueueIndex || !isPlaying()) {
if (DEBUG) {
Log.d(TAG, "Playback - Rewinding to correct "
+ "index=[" + currentPlayQueueIndex + "], "
diff --git a/app/src/main/java/org/schabi/newpipe/player/mediaitem/ExceptionTag.java b/app/src/main/java/org/schabi/newpipe/player/mediaitem/ExceptionTag.java
index 3e41262d660..7e28eda207e 100644
--- a/app/src/main/java/org/schabi/newpipe/player/mediaitem/ExceptionTag.java
+++ b/app/src/main/java/org/schabi/newpipe/player/mediaitem/ExceptionTag.java
@@ -87,16 +87,6 @@ public 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);
diff --git a/app/src/main/java/org/schabi/newpipe/player/mediaitem/MediaItemTag.java b/app/src/main/java/org/schabi/newpipe/player/mediaitem/MediaItemTag.java
index 6f9b9e9f245..8dbd552809f 100644
--- a/app/src/main/java/org/schabi/newpipe/player/mediaitem/MediaItemTag.java
+++ b/app/src/main/java/org/schabi/newpipe/player/mediaitem/MediaItemTag.java
@@ -44,9 +44,15 @@ public interface MediaItemTag {
StreamType getStreamType();
- Optional getMaybeStreamInfo();
+ @NonNull
+ default Optional getMaybeStreamInfo() {
+ return Optional.empty();
+ }
- Optional getMaybeQuality();
+ @NonNull
+ default Optional getMaybeQuality() {
+ return Optional.empty();
+ }
Optional getMaybeExtras(@NonNull Class type);
@@ -86,7 +92,7 @@ default MediaItem asMediaItem() {
.build();
}
- class Quality {
+ final class Quality {
@NonNull
private final List sortedVideoStreams;
private final int selectedVideoStreamIndex;
diff --git a/app/src/main/java/org/schabi/newpipe/player/mediaitem/PlaceholderTag.java b/app/src/main/java/org/schabi/newpipe/player/mediaitem/PlaceholderTag.java
index 5b53e5660d2..fe7aa9d9241 100644
--- a/app/src/main/java/org/schabi/newpipe/player/mediaitem/PlaceholderTag.java
+++ b/app/src/main/java/org/schabi/newpipe/player/mediaitem/PlaceholderTag.java
@@ -1,6 +1,5 @@
package org.schabi.newpipe.player.mediaitem;
-import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.util.Constants;
@@ -74,16 +73,6 @@ public 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);
diff --git a/app/src/main/java/org/schabi/newpipe/player/mediaitem/StreamInfoTag.java b/app/src/main/java/org/schabi/newpipe/player/mediaitem/StreamInfoTag.java
index 9ae25e07b35..6a942840d50 100644
--- a/app/src/main/java/org/schabi/newpipe/player/mediaitem/StreamInfoTag.java
+++ b/app/src/main/java/org/schabi/newpipe/player/mediaitem/StreamInfoTag.java
@@ -91,11 +91,13 @@ public StreamType getStreamType() {
return streamInfo.getStreamType();
}
+ @NonNull
@Override
public Optional getMaybeStreamInfo() {
return Optional.of(streamInfo);
}
+ @NonNull
@Override
public Optional getMaybeQuality() {
return Optional.ofNullable(quality);
diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java
index d68eb1a1a60..299ae845ddf 100644
--- a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java
+++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java
@@ -3,16 +3,16 @@
import android.util.Log;
import com.google.android.exoplayer2.MediaItem;
+import com.google.android.exoplayer2.PlaybackException;
import com.google.android.exoplayer2.Timeline;
-import com.google.android.exoplayer2.source.CompositeMediaSource;
+import com.google.android.exoplayer2.source.BaseMediaSource;
import com.google.android.exoplayer2.source.MediaPeriod;
-import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.SilenceMediaSource;
+import com.google.android.exoplayer2.source.SinglePeriodTimeline;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.TransferListener;
import org.schabi.newpipe.player.mediaitem.ExceptionTag;
-import org.schabi.newpipe.player.mediaitem.MediaItemTag;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import java.io.IOException;
@@ -22,7 +22,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-public class FailedMediaSource extends CompositeMediaSource implements ManagedMediaSource {
+public class FailedMediaSource extends BaseMediaSource implements ManagedMediaSource {
/**
* Play 2 seconds of silenced audio when a stream fails to resolve due to a known issue,
* such as {@link org.schabi.newpipe.extractor.exceptions.ExtractionException}.
@@ -32,12 +32,12 @@ public class FailedMediaSource extends CompositeMediaSource implements Man
* not recommended, it may cause ExoPlayer to buffer for a while.
* */
public static final long SILENCE_DURATION_US = TimeUnit.SECONDS.toMicros(2);
+ public static final MediaPeriod SILENT_MEDIA = makeSilentMediaPeriod(SILENCE_DURATION_US);
private final String TAG = "FailedMediaSource@" + Integer.toHexString(hashCode());
private final PlayQueueItem playQueueItem;
private final Throwable error;
private final long retryTimestamp;
- private final MediaSource source;
private final MediaItem mediaItem;
/**
* Fail the play queue item associated with this source, with potential future retries.
@@ -56,15 +56,10 @@ public FailedMediaSource(@NonNull final PlayQueueItem playQueueItem,
this.playQueueItem = playQueueItem;
this.error = error;
this.retryTimestamp = retryTimestamp;
-
- final MediaItemTag tag = ExceptionTag
+ this.mediaItem = ExceptionTag
.of(playQueueItem, Collections.singletonList(error))
- .withExtras(this);
- this.mediaItem = tag.asMediaItem();
- this.source = new SilenceMediaSource.Factory()
- .setDurationUs(SILENCE_DURATION_US)
- .setTag(tag)
- .createMediaSource();
+ .withExtras(this)
+ .asMediaItem();
}
public static FailedMediaSource of(@NonNull final PlayQueueItem playQueueItem,
@@ -91,49 +86,77 @@ private boolean canRetry() {
return System.currentTimeMillis() >= retryTimestamp;
}
- /**
- * Returns the {@link MediaItem} whose media is provided by the source.
- */
@Override
public MediaItem getMediaItem() {
return mediaItem;
}
+ /**
+ * Prepares the source with {@link Timeline} info on the silence playback when the error
+ * is classed as {@link FailedMediaSourceException}, for example, when the error is
+ * {@link org.schabi.newpipe.extractor.exceptions.ExtractionException ExtractionException}.
+ * These types of error are swallowed by {@link FailedMediaSource}, and the underlying
+ * exception is carried to the {@link MediaItem} metadata during playback.
+ *
+ * If the exception is not known, e.g. {@link java.net.UnknownHostException} or some
+ * other network issue, then no source info is refreshed and
+ * {@link #maybeThrowSourceInfoRefreshError()} be will triggered.
+ *
+ * Note that this method is called only once until {@link #releaseSourceInternal()} is called,
+ * so if no action is done in here, playback will stall unless
+ * {@link #maybeThrowSourceInfoRefreshError()} is called.
+ *
+ * @param mediaTransferListener No data transfer listener needed, ignored here.
+ */
@Override
protected void prepareSourceInternal(@Nullable final TransferListener mediaTransferListener) {
- super.prepareSourceInternal(mediaTransferListener);
Log.e(TAG, "Loading failed source: ", error);
if (error instanceof FailedMediaSourceException) {
- prepareChildSource(null, source);
+ refreshSourceInfo(makeSilentMediaTimeline(SILENCE_DURATION_US, mediaItem));
}
}
-
+ /**
+ * If the error is not known, e.g. network issue, then the exception is not swallowed here in
+ * {@link FailedMediaSource}. The exception is then propagated to the player, which
+ * {@link org.schabi.newpipe.player.Player Player} can react to inside
+ * {@link com.google.android.exoplayer2.Player.Listener#onPlayerError(PlaybackException)}.
+ *
+ * @throws IOException An error which will always result in
+ * {@link com.google.android.exoplayer2.PlaybackException#ERROR_CODE_IO_UNSPECIFIED}.
+ */
@Override
public void maybeThrowSourceInfoRefreshError() throws IOException {
if (!(error instanceof FailedMediaSourceException)) {
throw new IOException(error);
}
- super.maybeThrowSourceInfoRefreshError();
}
+ /**
+ * This method is only called if {@link #prepareSourceInternal(TransferListener)}
+ * refreshes the source info with no exception. All parameters are ignored as this
+ * returns a static and reused piece of silent audio.
+ *
+ * @param id The identifier of the period.
+ * @param allocator An {@link Allocator} from which to obtain media buffer allocations.
+ * @param startPositionUs The expected start position, in microseconds.
+ * @return The common {@link MediaPeriod} holding the silence.
+ */
@Override
- protected void onChildSourceInfoRefreshed(final Void id,
- final MediaSource mediaSource,
- final Timeline timeline) {
- refreshSourceInfo(timeline);
+ public MediaPeriod createPeriod(final MediaPeriodId id,
+ final Allocator allocator,
+ final long startPositionUs) {
+ return SILENT_MEDIA;
}
-
@Override
- public MediaPeriod createPeriod(final MediaPeriodId id, final Allocator allocator,
- final long startPositionUs) {
- return source.createPeriod(id, allocator, startPositionUs);
+ public void releasePeriod(final MediaPeriod mediaPeriod) {
+ /* Do Nothing (we want to keep re-using the Silent MediaPeriod) */
}
@Override
- public void releasePeriod(final MediaPeriod mediaPeriod) {
- source.releasePeriod(mediaPeriod);
+ protected void releaseSourceInternal() {
+ /* Do Nothing, no clean-up for processing/extra thread is needed by this MediaSource */
}
@Override
@@ -168,4 +191,22 @@ public StreamInfoLoadException(final Throwable cause) {
super(cause);
}
}
+
+ private static Timeline makeSilentMediaTimeline(final long durationUs,
+ @NonNull final MediaItem mediaItem) {
+ return new SinglePeriodTimeline(
+ durationUs,
+ /* isSeekable= */ true,
+ /* isDynamic= */ false,
+ /* useLiveConfiguration= */ false,
+ /* manifest= */ null,
+ mediaItem);
+ }
+
+ private static MediaPeriod makeSilentMediaPeriod(final long durationUs) {
+ return new SilenceMediaSource.Factory()
+ .setDurationUs(durationUs)
+ .createMediaSource()
+ .createPeriod(null, null, 0);
+ }
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java
index 193b90271ac..95524cf692a 100644
--- a/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java
+++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java
@@ -14,12 +14,24 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-public class LoadedMediaSource extends CompositeMediaSource implements ManagedMediaSource {
+public class LoadedMediaSource extends CompositeMediaSource implements ManagedMediaSource {
private final MediaSource source;
private final PlayQueueItem stream;
private final MediaItem mediaItem;
private final long expireTimestamp;
+ /**
+ * Uses a {@link CompositeMediaSource} to wrap one or more child {@link MediaSource}s
+ * containing actual media. This wrapper {@link LoadedMediaSource} holds the expiration
+ * timestamp as a {@link ManagedMediaSource} to allow explicit playlist management under
+ * {@link ManagedMediaSourcePlaylist}.
+ *
+ * @param source The child media source with actual media.
+ * @param tag Metadata for the child media source.
+ * @param stream The queue item associated with the media source.
+ * @param expireTimestamp The timestamp when the media source expires and might not be
+ * available for playback.
+ */
public LoadedMediaSource(@NonNull final MediaSource source,
@NonNull final MediaItemTag tag,
@NonNull final PlayQueueItem stream,
@@ -39,14 +51,35 @@ private boolean isExpired() {
return System.currentTimeMillis() >= expireTimestamp;
}
+ /**
+ * Delegates the preparation of child {@link MediaSource}s to the
+ * {@link CompositeMediaSource} wrapper. Since all {@link LoadedMediaSource}s use only
+ * a single child media, the child id of 0 is always used (sonar doesn't like null as id here).
+ *
+ * @param mediaTransferListener A data transfer listener that will be registered by the
+ * {@link CompositeMediaSource} for child source preparation.
+ */
@Override
protected void prepareSourceInternal(@Nullable final TransferListener mediaTransferListener) {
super.prepareSourceInternal(mediaTransferListener);
- prepareChildSource(null, source);
+ prepareChildSource(0, source);
}
+ /**
+ * When any child {@link MediaSource} is prepared, the refreshed {@link Timeline} can
+ * be listened to here. But since {@link LoadedMediaSource} has only a single child source,
+ * this method is called only once until {@link #releaseSourceInternal()} is called.
+ *
+ * On refresh, the {@link CompositeMediaSource} delegate will be notified with the
+ * new {@link Timeline}, otherwise {@link #createPeriod(MediaPeriodId, Allocator, long)}
+ * will not be called and playback may be stalled.
+ *
+ * @param id The unique id used to prepare the child source.
+ * @param mediaSource The child source whose source info has been refreshed.
+ * @param timeline The new timeline of the child source.
+ */
@Override
- protected void onChildSourceInfoRefreshed(final Void id,
+ protected void onChildSourceInfoRefreshed(final Integer id,
final MediaSource mediaSource,
final Timeline timeline) {
refreshSourceInfo(timeline);
diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java
index 6b3f91eb32c..92d4403c8b1 100644
--- a/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java
+++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java
@@ -18,9 +18,7 @@ final class PlaceholderMediaSource
private static final MediaItem MEDIA_ITEM = PlaceholderTag.EMPTY.withExtras(COPY).asMediaItem();
private PlaceholderMediaSource() { }
- /**
- * Returns the {@link MediaItem} whose media is provided by the source.
- */
+
@Override
public MediaItem getMediaItem() {
return MEDIA_ITEM;
@@ -30,7 +28,7 @@ public MediaItem getMediaItem() {
protected void onChildSourceInfoRefreshed(final Void id,
final MediaSource mediaSource,
final Timeline timeline) {
- /* Do nothing, no timeline updates will stall playback */
+ /* Do nothing, no timeline updates or error will stall playback */
}
@Override
From d289dc8a5331bfe941f61cbafa40c3d5328662b1 Mon Sep 17 00:00:00 2001
From: karyogamy
Date: Sat, 26 Mar 2022 20:17:52 -0400
Subject: [PATCH 4/5] updated: onPlayerError to not catch unspecified source
errors so notifications are created. updated: Throwable usage to Exceptions.
updated: minor styles and documentations.
---
.../org/schabi/newpipe/player/Player.java | 61 ++++++++++---------
.../player/mediaitem/ExceptionTag.java | 8 +--
.../player/mediaitem/MediaItemTag.java | 2 +-
.../player/mediaitem/PlaceholderTag.java | 2 +-
.../player/mediaitem/StreamInfoTag.java | 2 +-
.../player/mediasource/FailedMediaSource.java | 6 +-
.../player/playback/MediaSourceManager.java | 3 +-
7 files changed, 44 insertions(+), 40 deletions(-)
diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java
index ea0cab5e652..ba4c4f2379c 100644
--- a/app/src/main/java/org/schabi/newpipe/player/Player.java
+++ b/app/src/main/java/org/schabi/newpipe/player/Player.java
@@ -1961,13 +1961,12 @@ private void showOrHideButtons() {
final boolean showPrev = playQueue.getIndex() != 0;
final boolean showNext = playQueue.getIndex() + 1 != playQueue.getStreams().size();
final boolean showQueue = playQueue.getStreams().size() > 1 && !popupPlayerSelected();
- boolean showSegment = false;
- showSegment = /*only when stream has segment and playing in fullscreen player*/
- !popupPlayerSelected()
- && !getCurrentStreamInfo()
- .map(StreamInfo::getStreamSegments)
- .map(List::isEmpty)
- .orElse(/*no stream info=*/true);
+ /* only when stream has segments and is not playing in popup player */
+ final boolean showSegment = !popupPlayerSelected()
+ && !getCurrentStreamInfo()
+ .map(StreamInfo::getStreamSegments)
+ .map(List::isEmpty)
+ .orElse(/*no stream info=*/true);
binding.playPreviousButton.setVisibility(showPrev ? View.VISIBLE : View.INVISIBLE);
binding.playPreviousButton.setAlpha(showPrev ? 1.0f : 0.0f);
@@ -2014,7 +2013,7 @@ public void onPlayWhenReadyChanged(final boolean playWhenReady, final int reason
+ "playWhenReady = [" + playWhenReady + "], "
+ "reason = [" + reason + "]");
}
- final int playbackState = simpleExoPlayer == null
+ final int playbackState = exoPlayerIsNull()
? com.google.android.exoplayer2.Player.STATE_IDLE
: simpleExoPlayer.getPlaybackState();
updatePlaybackState(playWhenReady, playbackState);
@@ -2026,8 +2025,7 @@ public void onPlaybackStateChanged(final int playbackState) {
Log.d(TAG, "ExoPlayer - onPlaybackStateChanged() called with: "
+ "playbackState = [" + playbackState + "]");
}
- final boolean playWhenReady = simpleExoPlayer != null && simpleExoPlayer.getPlayWhenReady();
- updatePlaybackState(playWhenReady, playbackState);
+ updatePlaybackState(getPlayWhenReady(), playbackState);
}
private void updatePlaybackState(final boolean playWhenReady, final int playbackState) {
@@ -2486,6 +2484,19 @@ private void setMuteButton(@NonNull final ImageButton button, final boolean isMu
//////////////////////////////////////////////////////////////////////////*/
//region ExoPlayer listeners (that didn't fit in other categories)
+ /**
+ * Listens for event or state changes on ExoPlayer. When any event happens, we check for
+ * changes in the currently-playing metadata and update the encapsulating
+ * {@link Player}. Downstream listeners are also informed.
+ *
+ *
When the renewed metadata contains any error, it is reported as a notification.
+ * This is done because not all source resolution errors are {@link PlaybackException}, which
+ * are also captured by {@link ExoPlayer} and stops the playback.
+ *
+ * @param player The {@link com.google.android.exoplayer2.Player} whose state changed.
+ * @param events The {@link com.google.android.exoplayer2.Player.Events} that has triggered
+ * the player state changes.
+ **/
@Override
public void onEvents(@NonNull final com.google.android.exoplayer2.Player player,
@NonNull final com.google.android.exoplayer2.Player.Events events) {
@@ -2602,11 +2613,12 @@ public void onCues(@NonNull final List cues) {
* {@link PlaybackException#ERROR_CODE_BEHIND_LIVE_WINDOW BEHIND_LIVE_WINDOW}:
* If the playback on livestreams are lagged too far behind the current playable
* window. Then we seek to the latest timestamp and restart the playback.
+ * This error is catchable.
*
* From {@link PlaybackException#ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE BAD_IO} to
* {@link PlaybackException#ERROR_CODE_PARSING_MANIFEST_UNSUPPORTED UNSUPPORTED_FORMATS}:
* If the stream source is validated by the extractor but not recognized by the player,
- * then we can try to recover playback by signal an error on the {@link PlayQueue}.
+ * then we can try to recover playback by signalling an error on the {@link PlayQueue}.
* For {@link PlaybackException#ERROR_CODE_TIMEOUT PLAYER_TIMEOUT},
* {@link PlaybackException#ERROR_CODE_IO_UNSPECIFIED MEDIA_SOURCE_RESOLVER_TIMEOUT} and
* {@link PlaybackException#ERROR_CODE_IO_NETWORK_CONNECTION_FAILED NO_NETWORK}:
@@ -2617,8 +2629,8 @@ public void onCues(@NonNull final List cues) {
* We terminate the playback.
* For any other unspecified issue internal: We set a recovery and try to restart
* the playback.
- * In the case of decoder/renderer or unspecified errors, the player will create a
- * notification so the users are aware.
+ * For any error above that is not explicitly catchable, the player will
+ * create a notification so users are aware.
*
* @see com.google.android.exoplayer2.Player.Listener#onPlayerError(PlaybackException)
* */
@@ -2627,7 +2639,6 @@ public void onCues(@NonNull final List cues) {
public void onPlayerError(@NonNull final PlaybackException error) {
Log.e(TAG, "ExoPlayer - onPlayerError() called with:", error);
- setRecovery();
saveStreamProgressState();
boolean isCatchableException = false;
@@ -2652,7 +2663,6 @@ public void onPlayerError(@NonNull final PlaybackException error) {
case ERROR_CODE_PARSING_MANIFEST_UNSUPPORTED:
// Source errors, signal on playQueue and move on:
if (!exoPlayerIsNull() && playQueue != null) {
- isCatchableException = true;
playQueue.error();
}
break;
@@ -2660,11 +2670,6 @@ public void onPlayerError(@NonNull final PlaybackException error) {
case ERROR_CODE_IO_UNSPECIFIED:
case ERROR_CODE_IO_NETWORK_CONNECTION_FAILED:
case ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT:
- // Don't create notification on timeout/networking errors:
- isCatchableException = true;
- setRecovery();
- reloadPlayQueueManager();
- break;
case ERROR_CODE_UNSPECIFIED:
// Reload playback on unexpected errors:
setRecovery();
@@ -3010,10 +3015,9 @@ public void saveStreamProgressState() {
}
public void saveStreamProgressStateCompleted() {
- getCurrentStreamInfo().ifPresent(info -> {
- // current stream has ended, so the progress is its duration (+1 to overcome rounding)
- saveStreamProgressState((info.getDuration() + 1) * 1000);
- });
+ // current stream has ended, so the progress is its duration (+1 to overcome rounding)
+ getCurrentStreamInfo().ifPresent(info ->
+ saveStreamProgressState((info.getDuration() + 1) * 1000));
}
//endregion
@@ -3414,7 +3418,8 @@ private void updateStreamRelatedViews() {
case VIDEO_STREAM:
if (currentMetadata == null
|| !currentMetadata.getMaybeQuality().isPresent()
- || info.getVideoStreams().size() + info.getVideoOnlyStreams().size() == 0) {
+ || (info.getVideoStreams().isEmpty()
+ && info.getVideoOnlyStreams().isEmpty())) {
break;
}
@@ -3684,10 +3689,8 @@ private void onTextTracksChanged() {
}
// Normalize mismatching language strings
- final List preferredLanguages =
- trackSelector.getParameters().preferredTextLanguages;
- final String preferredLanguage =
- preferredLanguages.isEmpty() ? null : preferredLanguages.get(0);
+ final String preferredLanguage = trackSelector.getParameters()
+ .preferredTextLanguages.stream().findFirst().orElse(null);
// Build UI
buildCaptionMenu(availableLanguages);
if (trackSelector.getParameters().getRendererDisabled(textRenderer)
diff --git a/app/src/main/java/org/schabi/newpipe/player/mediaitem/ExceptionTag.java b/app/src/main/java/org/schabi/newpipe/player/mediaitem/ExceptionTag.java
index 7e28eda207e..ebedf8c71f6 100644
--- a/app/src/main/java/org/schabi/newpipe/player/mediaitem/ExceptionTag.java
+++ b/app/src/main/java/org/schabi/newpipe/player/mediaitem/ExceptionTag.java
@@ -24,12 +24,12 @@ public final class ExceptionTag implements MediaItemTag {
@NonNull
private final PlayQueueItem item;
@NonNull
- private final List errors;
+ private final List errors;
@Nullable
private final Object extras;
private ExceptionTag(@NonNull final PlayQueueItem item,
- @NonNull final List errors,
+ @NonNull final List errors,
@Nullable final Object extras) {
this.item = item;
this.errors = errors;
@@ -37,13 +37,13 @@ private ExceptionTag(@NonNull final PlayQueueItem item,
}
public static ExceptionTag of(@NonNull final PlayQueueItem playQueueItem,
- @NonNull final List errors) {
+ @NonNull final List errors) {
return new ExceptionTag(playQueueItem, errors, null);
}
@NonNull
@Override
- public List getErrors() {
+ public List getErrors() {
return errors;
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/mediaitem/MediaItemTag.java b/app/src/main/java/org/schabi/newpipe/player/mediaitem/MediaItemTag.java
index 8dbd552809f..f84b0383adb 100644
--- a/app/src/main/java/org/schabi/newpipe/player/mediaitem/MediaItemTag.java
+++ b/app/src/main/java/org/schabi/newpipe/player/mediaitem/MediaItemTag.java
@@ -26,7 +26,7 @@
**/
public interface MediaItemTag {
- List getErrors();
+ List getErrors();
int getServiceId();
diff --git a/app/src/main/java/org/schabi/newpipe/player/mediaitem/PlaceholderTag.java b/app/src/main/java/org/schabi/newpipe/player/mediaitem/PlaceholderTag.java
index fe7aa9d9241..cce4e9f17f6 100644
--- a/app/src/main/java/org/schabi/newpipe/player/mediaitem/PlaceholderTag.java
+++ b/app/src/main/java/org/schabi/newpipe/player/mediaitem/PlaceholderTag.java
@@ -29,7 +29,7 @@ private PlaceholderTag(@Nullable final Object extras) {
@NonNull
@Override
- public List getErrors() {
+ public List getErrors() {
return Collections.emptyList();
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/mediaitem/StreamInfoTag.java b/app/src/main/java/org/schabi/newpipe/player/mediaitem/StreamInfoTag.java
index 6a942840d50..4095f2bc888 100644
--- a/app/src/main/java/org/schabi/newpipe/player/mediaitem/StreamInfoTag.java
+++ b/app/src/main/java/org/schabi/newpipe/player/mediaitem/StreamInfoTag.java
@@ -47,7 +47,7 @@ public static StreamInfoTag of(@NonNull final StreamInfo streamInfo) {
}
@Override
- public List getErrors() {
+ public List getErrors() {
return Collections.emptyList();
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java
index 299ae845ddf..fa52ab0ee4c 100644
--- a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java
+++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java
@@ -36,7 +36,7 @@ public class FailedMediaSource extends BaseMediaSource implements ManagedMediaSo
private final String TAG = "FailedMediaSource@" + Integer.toHexString(hashCode());
private final PlayQueueItem playQueueItem;
- private final Throwable error;
+ private final Exception error;
private final long retryTimestamp;
private final MediaItem mediaItem;
/**
@@ -51,7 +51,7 @@ public class FailedMediaSource extends BaseMediaSource implements ManagedMediaSo
* @param retryTimestamp epoch timestamp when this MediaSource can be refreshed
*/
public FailedMediaSource(@NonNull final PlayQueueItem playQueueItem,
- @NonNull final Throwable error,
+ @NonNull final Exception error,
final long retryTimestamp) {
this.playQueueItem = playQueueItem;
this.error = error;
@@ -68,7 +68,7 @@ public static FailedMediaSource of(@NonNull final PlayQueueItem playQueueItem,
}
public static FailedMediaSource of(@NonNull final PlayQueueItem playQueueItem,
- @NonNull final Throwable error,
+ @NonNull final Exception error,
final long retryWaitMillis) {
return new FailedMediaSource(playQueueItem, error,
System.currentTimeMillis() + retryWaitMillis);
diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java
index d4ed973aa59..b4e9a15ab91 100644
--- a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java
+++ b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java
@@ -441,7 +441,8 @@ private Single getLoadedMediaSource(@NonNull final PlayQueue
if (throwable instanceof ExtractionException) {
return FailedMediaSource.of(stream, new StreamInfoLoadException(throwable));
}
- return FailedMediaSource.of(stream, throwable, /*immediatelyRetryable=*/0L);
+ return FailedMediaSource
+ .of(stream, new Exception(throwable), /*immediatelyRetryable=*/0L);
});
}
From a00bc95acccd5c4c253726ecb4decb21d8cd228f Mon Sep 17 00:00:00 2001
From: karyogamy
Date: Sun, 27 Mar 2022 13:24:37 -0400
Subject: [PATCH 5/5] updated: source loading error for FailedMediaSource to
wait for 3 seconds before allowing retry. updated: minor style fixes.
---
app/src/main/java/org/schabi/newpipe/player/Player.java | 7 +++++--
.../newpipe/player/mediasource/FailedMediaSource.java | 2 +-
.../schabi/newpipe/player/playback/MediaSourceManager.java | 6 ++++--
3 files changed, 10 insertions(+), 5 deletions(-)
diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java
index ba4c4f2379c..c4205124cc2 100644
--- a/app/src/main/java/org/schabi/newpipe/player/Player.java
+++ b/app/src/main/java/org/schabi/newpipe/player/Player.java
@@ -2487,11 +2487,11 @@ private void setMuteButton(@NonNull final ImageButton button, final boolean isMu
/**
* Listens for event or state changes on ExoPlayer. When any event happens, we check for
* changes in the currently-playing metadata and update the encapsulating
- * {@link Player}. Downstream listeners are also informed.
+ * {@link Player}. Downstream listeners are also informed.
*
* When the renewed metadata contains any error, it is reported as a notification.
* This is done because not all source resolution errors are {@link PlaybackException}, which
- * are also captured by {@link ExoPlayer} and stops the playback.
+ * are also captured by {@link ExoPlayer} and stops the playback.
*
* @param player The {@link com.google.android.exoplayer2.Player} whose state changed.
* @param events The {@link com.google.android.exoplayer2.Player.Events} that has triggered
@@ -2634,6 +2634,9 @@ public void onCues(@NonNull final List cues) {
*
* @see com.google.android.exoplayer2.Player.Listener#onPlayerError(PlaybackException)
* */
+ // Any error code not explicitly covered here are either unrelated to NewPipe use case
+ // (e.g. DRM) or not recoverable (e.g. Decoder error). In both cases, the player should
+ // shutdown.
@SuppressLint("SwitchIntDef")
@Override
public void onPlayerError(@NonNull final PlaybackException error) {
diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java
index fa52ab0ee4c..8aad356d0ae 100644
--- a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java
+++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java
@@ -78,7 +78,7 @@ public PlayQueueItem getStream() {
return playQueueItem;
}
- public Throwable getError() {
+ public Exception getError() {
return error;
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java
index b4e9a15ab91..9b13bb3d7d7 100644
--- a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java
+++ b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java
@@ -441,8 +441,10 @@ private Single getLoadedMediaSource(@NonNull final PlayQueue
if (throwable instanceof ExtractionException) {
return FailedMediaSource.of(stream, new StreamInfoLoadException(throwable));
}
- return FailedMediaSource
- .of(stream, new Exception(throwable), /*immediatelyRetryable=*/0L);
+ // Non-source related error expected here (e.g. network),
+ // should allow retry shortly after the error.
+ return FailedMediaSource.of(stream, new Exception(throwable),
+ /*allowRetryIn=*/TimeUnit.MILLISECONDS.convert(3, TimeUnit.SECONDS));
});
}