Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add seamless transition between background and video players when putting the app in background (for video-only streams and audio-only streams only) #7349

Merged
merged 16 commits into from
Feb 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/src/main/java/org/schabi/newpipe/RouterActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -633,7 +633,7 @@ private void openDownloadDialog() {
.subscribe(result -> {
final List<VideoStream> sortedVideoStreams = ListHelper
.getSortedStreamVideosList(this, result.getVideoStreams(),
result.getVideoOnlyStreams(), false);
result.getVideoOnlyStreams(), false, false);
final int selectedVideoStreamIndex = ListHelper
.getDefaultResolutionIndex(this, sortedVideoStreams);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ public static DownloadDialog newInstance(final StreamInfo info) {
public static DownloadDialog newInstance(final Context context, final StreamInfo info) {
final ArrayList<VideoStream> streamsList = new ArrayList<>(ListHelper
.getSortedStreamVideosList(context, info.getVideoStreams(),
info.getVideoOnlyStreams(), false));
info.getVideoOnlyStreams(), false, false));
final int selectedStreamIndex = ListHelper.getDefaultResolutionIndex(context, streamsList);

final DownloadDialog instance = newInstance(info);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1617,6 +1617,7 @@ public void handleResult(@NonNull final StreamInfo info) {
activity,
info.getVideoStreams(),
info.getVideoOnlyStreams(),
false,
false);
selectedVideoStreamIndex = ListHelper
.getDefaultResolutionIndex(activity, sortedVideoStreams);
Expand Down
211 changes: 185 additions & 26 deletions app/src/main/java/org/schabi/newpipe/player/Player.java
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@
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;
Expand All @@ -122,6 +123,7 @@
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;
Expand All @@ -144,6 +146,7 @@
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;
Expand Down Expand Up @@ -175,6 +178,7 @@
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;
import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHolder;
import org.schabi.newpipe.util.DeviceUtils;
Expand All @@ -193,6 +197,7 @@
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.IntStream;

import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Observable;
Expand Down Expand Up @@ -2443,9 +2448,9 @@ public void onPlaybackParametersChanged(@NonNull final PlaybackParameters playba
}

@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 + "]");
Expand Down Expand Up @@ -2493,7 +2498,7 @@ public void onRenderedFirstFrame() {
}

@Override
public void onCues(final List<Cue> cues) {
public void onCues(@NonNull final List<Cue> cues) {
binding.subtitleView.onCues(cues);
}
//endregion
Expand Down Expand Up @@ -2999,18 +3004,19 @@ private void maybeUpdateCurrentMetadata() {

final MediaSourceTag metadata;
try {
metadata = (MediaSourceTag) simpleExoPlayer.getCurrentTag();
} catch (IndexOutOfBoundsException | ClassCastException error) {
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: " + error.getMessage());
error.printStackTrace();
Log.d(TAG, "Could not update metadata", ex);
}
return;
}

if (metadata == null) {
return;
}
maybeAutoQueueNextStream(metadata);

if (currentMetadata == metadata) {
Expand Down Expand Up @@ -3286,7 +3292,27 @@ public void onStartDrag(final PlayQueueItemHolder viewHolder) {
@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);
}

if (isAudioOnly && videoResolver.getStreamSourceType().orElse(
SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY)
== SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY) {
// If the current info has only video streams with audio and if the stream is played as
// audio, we need to use the audio resolver, otherwise the video stream will be played
// in background.
return audioResolver.resolve(info);
}

// Even if the stream is played in background, we need to use the video resolver if the
// info played is separated video-only and audio-only streams; otherwise, if the audio
// resolver was called when the app was in background, the app will only stream audio when
// the user come back to the app and will never fetch the video stream.
// Note that the video is not fetched when the app is in background because the video
// renderer is fully disabled (see useVideoSource method), except for HLS streams
// (see https://github.com/google/ExoPlayer/issues/9282).
return videoResolver.resolve(info);
}

public void disablePreloadingOfCurrentTrack() {
Expand Down Expand Up @@ -4141,19 +4167,125 @@ public AppCompatActivity getParentActivity() {
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) {
AudricV marked this conversation as resolved.
Show resolved Hide resolved
if (playQueue == null || isAudioOnly == !videoEnabled || audioPlayerSelected()) {
return;
}

isAudioOnly = !video;
// When a user returns from background controls could be hidden
// but systemUI will be shown 100%. Hide it
isAudioOnly = !videoEnabled;
// When a user returns from background, controls could be hidden but SystemUI will be shown
// 100%. Hide it.
if (!isAudioOnly && !isControlsVisible()) {
hideSystemUIIfNeeded();
}

// The current metadata may be null sometimes (for e.g. when using an unstable connection
// 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) {
reloadPlayQueueManager();
setRecovery();
return;
}

final int videoRenderIndex = getVideoRendererIndex();
final StreamInfo info = currentMetadata.getMetadata();

// In the case we don't know the source type, fallback to the one with video with audio or
// audio-only source.
final SourceType sourceType = videoResolver.getStreamSourceType().orElse(
SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY);

if (playQueueManagerReloadingNeeded(sourceType, info, videoRenderIndex)) {
reloadPlayQueueManager();
} else {
final StreamType streamType = info.getStreamType();
if (streamType == StreamType.AUDIO_STREAM
|| streamType == StreamType.AUDIO_LIVE_STREAM) {
// Nothing to do more than setting the recovery position
setRecovery();
return;
}

final TrackGroupArray videoTrackGroupArray = Objects.requireNonNull(
trackSelector.getCurrentMappedTrackInfo()).getTrackGroups(videoRenderIndex);
if (videoEnabled) {
// Clearing the null selection override enable again the video stream (and its
// fetching).
trackSelector.setParameters(trackSelector.buildUponParameters()
.clearSelectionOverride(videoRenderIndex, videoTrackGroupArray));
} else {
// Using setRendererDisabled still fetch the video stream in background, contrary
// to setSelectionOverride with a null override.
trackSelector.setParameters(trackSelector.buildUponParameters()
.setSelectionOverride(videoRenderIndex, videoTrackGroupArray, null));
}
}

setRecovery();
reloadPlayQueueManager();
}

/**
* Return whether the play queue manager needs to be reloaded when switching player type.
AudricV marked this conversation as resolved.
Show resolved Hide resolved
*
* <p>
* The play queue manager needs to be reloaded if the video renderer index is not known and if
* the content is not an audio content, but also if none of the following cases is met:
*
* <ul>
* <li>the content is an {@link StreamType#AUDIO_STREAM audio stream} or an
* {@link StreamType#AUDIO_LIVE_STREAM audio live stream};</li>
* <li>the content is a {@link StreamType#LIVE_STREAM live stream} and the source type is a
* {@link SourceType#LIVE_STREAM live source};</li>
* <li>the content's source is {@link SourceType#VIDEO_WITH_SEPARATED_AUDIO a video stream
* with a separated audio source} or has no audio-only streams available <b>and</b> is a
* {@link StreamType#LIVE_STREAM live stream} or a
* {@link StreamType#LIVE_STREAM live stream}.
* </li>
* </ul>
* </p>
*
* @param sourceType the {@link SourceType} of the stream
* @param streamInfo the {@link StreamInfo} of the stream
* @param videoRendererIndex the video renderer index of the video source, if that's a video
* source (or {@link #RENDERER_UNAVAILABLE})
* @return whether the play queue manager needs to be reloaded
*/
private boolean playQueueManagerReloadingNeeded(final SourceType sourceType,
@NonNull final StreamInfo streamInfo,
final int videoRendererIndex) {
final StreamType streamType = streamInfo.getStreamType();

if (videoRendererIndex == RENDERER_UNAVAILABLE && streamType != StreamType.AUDIO_STREAM
&& streamType != StreamType.AUDIO_LIVE_STREAM) {
return true;
}

// The content is an audio stream, an audio live stream, or a live stream with a live
// source: it's not needed to reload the play queue manager because the stream source will
// be the same
if ((streamType == StreamType.AUDIO_STREAM || streamType == StreamType.AUDIO_LIVE_STREAM)
|| (streamType == StreamType.LIVE_STREAM
&& sourceType == SourceType.LIVE_STREAM)) {
return false;
}

// The content's source is a video with separated audio or a video with audio -> the video
// and its fetch may be disabled
// The content's source is a video with embedded audio and the content has no separated
// audio stream available: it's probably not needed to reload the play queue manager
// because the stream source will be probably the same as the current played
if (sourceType == SourceType.VIDEO_WITH_SEPARATED_AUDIO
|| (sourceType == SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY
&& isNullOrEmpty(streamInfo.getAudioStreams()))) {
// It's not needed to reload the play queue manager only if the content's stream type
// is a video stream or a live stream
return streamType != StreamType.VIDEO_STREAM && streamType != StreamType.LIVE_STREAM;
}

// Other cases: the play queue manager reload is needed
return true;
}
//endregion

Expand Down Expand Up @@ -4191,7 +4323,7 @@ private boolean isLoading() {
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);
Expand Down Expand Up @@ -4369,15 +4501,42 @@ private void setupVideoSurface() {
}

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

/**
* Get the video renderer index of the current playing stream.
*
* This method returns the video renderer index of the current
* {@link MappingTrackSelector.MappedTrackInfo} or {@link #RENDERER_UNAVAILABLE} if the current
* {@link MappingTrackSelector.MappedTrackInfo} is null or if there is no video renderer index.
*
* @return the video renderer index or {@link #RENDERER_UNAVAILABLE} if it cannot be get
*/
private int getVideoRendererIndex() {
AudricV marked this conversation as resolved.
Show resolved Hide resolved
final MappingTrackSelector.MappedTrackInfo mappedTrackInfo = trackSelector
.getCurrentMappedTrackInfo();

if (mappedTrackInfo == null) {
return RENDERER_UNAVAILABLE;
}

// Check every renderer
return IntStream.range(0, mappedTrackInfo.getRendererCount())
// Check the renderer is a video renderer and has at least one track
.filter(i -> !mappedTrackInfo.getTrackGroups(i).isEmpty()
&& simpleExoPlayer.getRendererType(i) == C.TRACK_TYPE_VIDEO)
// Return the first index found (there is at most one renderer per renderer type)
.findFirst()
// No video renderer index with at least one track found: return unavailable index
.orElse(RENDERER_UNAVAILABLE);
}
}
Loading