From 336f9f3813837746a5642a21f92966dda68bbf0a Mon Sep 17 00:00:00 2001 From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com> Date: Wed, 27 Oct 2021 20:28:53 +0200 Subject: [PATCH] Add seamless transition between background player and video players (for video-only and audio-only streams only) This is only available when playing video-only streams (and when there is no audio stream and only video streams with audio) and audio-only streams. For more details about which conditions are required to get this transition, look at the changes in the useVideoSource(boolean) method of the Player class. --- .../org/schabi/newpipe/player/Player.java | 115 +++++++++++++++--- .../resolver/AudioPlaybackResolver.java | 5 + .../newpipe/player/resolver/Resolver.java | 2 + .../resolver/VideoPlaybackResolver.java | 9 ++ 4 files changed, 112 insertions(+), 19 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 e0debc4e7..bec120baa 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -112,6 +112,7 @@ import androidx.recyclerview.widget.RecyclerView; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player.PositionInfo; import com.google.android.exoplayer2.RenderersFactory; @@ -122,6 +123,7 @@ 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.MappingTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import com.google.android.exoplayer2.ui.CaptionStyleCompat; @@ -144,6 +146,7 @@ import org.schabi.newpipe.error.UserAction; import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamSegment; +import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; import org.schabi.newpipe.fragments.detail.VideoDetailFragment; @@ -2443,9 +2446,9 @@ public final class Player implements } @Override - public void onPositionDiscontinuity( - final PositionInfo oldPosition, final PositionInfo newPosition, - @DiscontinuityReason final int discontinuityReason) { + public void onPositionDiscontinuity(@NonNull final PositionInfo oldPosition, + @NonNull final PositionInfo newPosition, + @DiscontinuityReason final int discontinuityReason) { if (DEBUG) { Log.d(TAG, "ExoPlayer - onPositionDiscontinuity() called with " + "discontinuityReason = [" + discontinuityReason + "]"); @@ -2493,7 +2496,7 @@ public final class Player implements } @Override - public void onCues(final List cues) { + public void onCues(@NonNull final List cues) { binding.subtitleView.onCues(cues); } //endregion @@ -2999,8 +3002,16 @@ public final class Player implements final MediaSourceTag metadata; try { - metadata = (MediaSourceTag) simpleExoPlayer.getCurrentTag(); - } catch (IndexOutOfBoundsException | ClassCastException error) { + final MediaItem currentMediaItem = simpleExoPlayer.getCurrentMediaItem(); + if (currentMediaItem != null) { + final MediaItem.PlaybackProperties playbackProperties = + currentMediaItem.playbackProperties; + metadata = (MediaSourceTag) (playbackProperties != null ? playbackProperties.tag + : null); + } else { + metadata = null; + } + } catch (final IndexOutOfBoundsException | ClassCastException error) { if (DEBUG) { Log.d(TAG, "Could not update metadata: " + error.getMessage()); error.printStackTrace(); @@ -3286,7 +3297,15 @@ public final class Player implements @Override // own playback listener @Nullable public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) { - return (isAudioOnly ? audioResolver : videoResolver).resolve(info); + if (audioPlayerSelected()) { + return audioResolver.resolve(info); + } else { + if (isAudioOnly && !videoResolver.isVideoStreamVideoOnly()) { + return audioResolver.resolve(info); + } + + return videoResolver.resolve(info); + } } public void disablePreloadingOfCurrentTrack() { @@ -4141,19 +4160,61 @@ public final class Player implements return (AppCompatActivity) ((ViewGroup) binding.getRoot().getParent()).getContext(); } - private void useVideoSource(final boolean video) { - if (playQueue == null || isAudioOnly == !video || audioPlayerSelected()) { + private void useVideoSource(final boolean videoEnabled) { + if (playQueue == null || isAudioOnly == !videoEnabled || audioPlayerSelected()) { return; } - isAudioOnly = !video; + isAudioOnly = !videoEnabled; // When a user returns from background controls could be hidden // but systemUI will be shown 100%. Hide it if (!isAudioOnly && !isControlsVisible()) { hideSystemUIIfNeeded(); } + + final int videoRenderIndex = getVideoRendererIndex(); + + // We can safely assume that currentMetadata is not null (otherwise this method isn't + // called) so we can use the requireNonNull method of the Objects class. + final StreamInfo info = Objects.requireNonNull(currentMetadata).getMetadata(); + + /* For video streams: we don't want to stream in background the video stream so if the + video stream played is not a video-only stream and if there is an audio stream available, + play this audio stream in background by reloading the play queue manager. + Otherwise the video renderer will be just disabled (because there is no + other stream for it to play the audio): if the video stream is video-only, only the audio + stream will be fetched and the video stream will be fetched again when the user return to a + video player. + + For audio streams: nothing is done, it's not needed to reload the player with the same + audio stream. + + In the case where we don't know the index of the video renderer, the play queue manager + is also reloaded. */ + + final StreamType streamType = info.getStreamType(); + + final boolean isVideoStreamTypeAndIsVideoOnlyStreamOrNoAudioStreamsAvailable = + (streamType == StreamType.VIDEO_STREAM || streamType == StreamType.LIVE_STREAM) + && (videoResolver.isVideoStreamVideoOnly() + || isNullOrEmpty(info.getAudioStreams())); + if (videoRenderIndex != RENDERER_UNAVAILABLE + && isVideoStreamTypeAndIsVideoOnlyStreamOrNoAudioStreamsAvailable) { + final TrackGroupArray videoTrackGroupArray = Objects.requireNonNull( + trackSelector.getCurrentMappedTrackInfo()).getTrackGroups(videoRenderIndex); + if (videoEnabled) { + trackSelector.setParameters(trackSelector.buildUponParameters() + .clearSelectionOverride(videoRenderIndex, videoTrackGroupArray)); + } else { + trackSelector.setParameters(trackSelector.buildUponParameters() + .setSelectionOverride(videoRenderIndex, videoTrackGroupArray, null)); + } + } else if (streamType != StreamType.AUDIO_STREAM + && streamType != StreamType.AUDIO_LIVE_STREAM) { + reloadPlayQueueManager(); + } + setRecovery(); - reloadPlayQueueManager(); } //endregion @@ -4191,7 +4252,7 @@ public final class Player implements private boolean isLive() { try { return !exoPlayerIsNull() && simpleExoPlayer.isCurrentWindowDynamic(); - } catch (@NonNull final IndexOutOfBoundsException e) { + } catch (final IndexOutOfBoundsException e) { // Why would this even happen =(... but lets log it anyway, better safe than sorry if (DEBUG) { Log.d(TAG, "player.isCurrentWindowDynamic() failed: ", e); @@ -4369,15 +4430,31 @@ public final class Player implements } private void cleanupVideoSurface() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // >=API23 - if (surfaceHolderCallback != null) { - if (binding != null) { - binding.surfaceView.getHolder().removeCallback(surfaceHolderCallback); - } - surfaceHolderCallback.release(); - surfaceHolderCallback = null; + // Only for API >= 23 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && surfaceHolderCallback != null) { + if (binding != null) { + binding.surfaceView.getHolder().removeCallback(surfaceHolderCallback); } + surfaceHolderCallback.release(); + surfaceHolderCallback = null; } } //endregion + + private int getVideoRendererIndex() { + final MappingTrackSelector.MappedTrackInfo mappedTrackInfo = trackSelector + .getCurrentMappedTrackInfo(); + + if (mappedTrackInfo != null) { + for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) { + final TrackGroupArray trackGroups = mappedTrackInfo.getTrackGroups(i); + if (!trackGroups.isEmpty() + && simpleExoPlayer.getRendererType(i) == C.TRACK_TYPE_VIDEO) { + return i; + } + } + } + + return RENDERER_UNAVAILABLE; + } } diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java index 29be402c5..17ecac43e 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 @@ -44,4 +44,9 @@ public class AudioPlaybackResolver implements PlaybackResolver { return buildMediaSource(dataSource, audio.getUrl(), PlayerHelper.cacheKeyOf(info, audio), MediaFormat.getSuffixById(audio.getFormatId()), tag); } + + @Override + public boolean isVideoStreamVideoOnly() { + return false; + } } diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/Resolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/Resolver.java index a3e1db5b4..8bfe34896 100644 --- a/app/src/main/java/org/schabi/newpipe/player/resolver/Resolver.java +++ b/app/src/main/java/org/schabi/newpipe/player/resolver/Resolver.java @@ -6,4 +6,6 @@ import androidx.annotation.Nullable; public interface Resolver { @Nullable Product resolve(@NonNull Source source); + + boolean isVideoStreamVideoOnly(); } 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 245a85e71..f0131c340 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 @@ -35,6 +35,8 @@ public class VideoPlaybackResolver implements PlaybackResolver { @Nullable private String playbackQuality; + private boolean isVideoStreamVideoOnly = false; + public VideoPlaybackResolver(@NonNull final Context context, @NonNull final PlayerDataSource dataSource, @NonNull final QualityResolver qualityResolver) { @@ -46,6 +48,7 @@ public class VideoPlaybackResolver implements PlaybackResolver { @Override @Nullable public MediaSource resolve(@NonNull final StreamInfo info) { + isVideoStreamVideoOnly = false; final MediaSource liveSource = maybeBuildLiveMediaSource(dataSource, info); if (liveSource != null) { return liveSource; @@ -85,6 +88,7 @@ public class VideoPlaybackResolver implements PlaybackResolver { PlayerHelper.cacheKeyOf(info, audio), MediaFormat.getSuffixById(audio.getFormatId()), tag); mediaSources.add(audioSource); + isVideoStreamVideoOnly = true; } // If there is no audio or video sources, then this media source cannot be played back @@ -118,6 +122,11 @@ public class VideoPlaybackResolver implements PlaybackResolver { } } + @Override + public boolean isVideoStreamVideoOnly() { + return isVideoStreamVideoOnly; + } + @Nullable public String getPlaybackQuality() { return playbackQuality;