Skip to content

Commit a95318a

Browse files
authored
Merge pull request #7349 from TiA4f8R/seamless-transition-players
Add seamless transition between background and video players when putting the app in background (for video-only streams and audio-only streams only)
2 parents 46fad32 + c5fc371 commit a95318a

9 files changed

Lines changed: 392 additions & 146 deletions

File tree

app/src/main/java/org/schabi/newpipe/RouterActivity.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -633,7 +633,7 @@ private void openDownloadDialog() {
633633
.subscribe(result -> {
634634
final List<VideoStream> sortedVideoStreams = ListHelper
635635
.getSortedStreamVideosList(this, result.getVideoStreams(),
636-
result.getVideoOnlyStreams(), false);
636+
result.getVideoOnlyStreams(), false, false);
637637
final int selectedVideoStreamIndex = ListHelper
638638
.getDefaultResolutionIndex(this, sortedVideoStreams);
639639

app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ public static DownloadDialog newInstance(final StreamInfo info) {
151151
public static DownloadDialog newInstance(final Context context, final StreamInfo info) {
152152
final ArrayList<VideoStream> streamsList = new ArrayList<>(ListHelper
153153
.getSortedStreamVideosList(context, info.getVideoStreams(),
154-
info.getVideoOnlyStreams(), false));
154+
info.getVideoOnlyStreams(), false, false));
155155
final int selectedStreamIndex = ListHelper.getDefaultResolutionIndex(context, streamsList);
156156

157157
final DownloadDialog instance = newInstance(info);

app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1617,6 +1617,7 @@ public void handleResult(@NonNull final StreamInfo info) {
16171617
activity,
16181618
info.getVideoStreams(),
16191619
info.getVideoOnlyStreams(),
1620+
false,
16201621
false);
16211622
selectedVideoStreamIndex = ListHelper
16221623
.getDefaultResolutionIndex(activity, sortedVideoStreams);

app/src/main/java/org/schabi/newpipe/player/Player.java

Lines changed: 185 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@
112112
import com.google.android.exoplayer2.C;
113113
import com.google.android.exoplayer2.DefaultRenderersFactory;
114114
import com.google.android.exoplayer2.ExoPlaybackException;
115+
import com.google.android.exoplayer2.MediaItem;
115116
import com.google.android.exoplayer2.PlaybackParameters;
116117
import com.google.android.exoplayer2.Player.PositionInfo;
117118
import com.google.android.exoplayer2.RenderersFactory;
@@ -122,6 +123,7 @@
122123
import com.google.android.exoplayer2.source.TrackGroup;
123124
import com.google.android.exoplayer2.source.TrackGroupArray;
124125
import com.google.android.exoplayer2.text.Cue;
126+
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
125127
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
126128
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
127129
import com.google.android.exoplayer2.ui.CaptionStyleCompat;
@@ -144,6 +146,7 @@
144146
import org.schabi.newpipe.extractor.MediaFormat;
145147
import org.schabi.newpipe.extractor.stream.StreamInfo;
146148
import org.schabi.newpipe.extractor.stream.StreamSegment;
149+
import org.schabi.newpipe.extractor.stream.StreamType;
147150
import org.schabi.newpipe.extractor.stream.VideoStream;
148151
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
149152
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
@@ -175,6 +178,7 @@
175178
import org.schabi.newpipe.player.resolver.AudioPlaybackResolver;
176179
import org.schabi.newpipe.player.resolver.MediaSourceTag;
177180
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver;
181+
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver.SourceType;
178182
import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper;
179183
import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHolder;
180184
import org.schabi.newpipe.util.DeviceUtils;
@@ -193,6 +197,7 @@
193197
import java.util.List;
194198
import java.util.Objects;
195199
import java.util.Optional;
200+
import java.util.stream.IntStream;
196201

197202
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
198203
import io.reactivex.rxjava3.core.Observable;
@@ -2449,9 +2454,9 @@ public void onPlaybackParametersChanged(@NonNull final PlaybackParameters playba
24492454
}
24502455

24512456
@Override
2452-
public void onPositionDiscontinuity(
2453-
final PositionInfo oldPosition, final PositionInfo newPosition,
2454-
@DiscontinuityReason final int discontinuityReason) {
2457+
public void onPositionDiscontinuity(@NonNull final PositionInfo oldPosition,
2458+
@NonNull final PositionInfo newPosition,
2459+
@DiscontinuityReason final int discontinuityReason) {
24552460
if (DEBUG) {
24562461
Log.d(TAG, "ExoPlayer - onPositionDiscontinuity() called with "
24572462
+ "discontinuityReason = [" + discontinuityReason + "]");
@@ -2499,7 +2504,7 @@ public void onRenderedFirstFrame() {
24992504
}
25002505

25012506
@Override
2502-
public void onCues(final List<Cue> cues) {
2507+
public void onCues(@NonNull final List<Cue> cues) {
25032508
binding.subtitleView.onCues(cues);
25042509
}
25052510
//endregion
@@ -3005,18 +3010,19 @@ private void maybeUpdateCurrentMetadata() {
30053010

30063011
final MediaSourceTag metadata;
30073012
try {
3008-
metadata = (MediaSourceTag) simpleExoPlayer.getCurrentTag();
3009-
} catch (IndexOutOfBoundsException | ClassCastException error) {
3013+
final MediaItem currentMediaItem = simpleExoPlayer.getCurrentMediaItem();
3014+
if (currentMediaItem == null || currentMediaItem.playbackProperties == null
3015+
|| currentMediaItem.playbackProperties.tag == null) {
3016+
return;
3017+
}
3018+
metadata = (MediaSourceTag) currentMediaItem.playbackProperties.tag;
3019+
} catch (final IndexOutOfBoundsException | ClassCastException ex) {
30103020
if (DEBUG) {
3011-
Log.d(TAG, "Could not update metadata: " + error.getMessage());
3012-
error.printStackTrace();
3021+
Log.d(TAG, "Could not update metadata", ex);
30133022
}
30143023
return;
30153024
}
30163025

3017-
if (metadata == null) {
3018-
return;
3019-
}
30203026
maybeAutoQueueNextStream(metadata);
30213027

30223028
if (currentMetadata == metadata) {
@@ -3292,7 +3298,27 @@ public void onStartDrag(final PlayQueueItemHolder viewHolder) {
32923298
@Override // own playback listener
32933299
@Nullable
32943300
public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) {
3295-
return (isAudioOnly ? audioResolver : videoResolver).resolve(info);
3301+
if (audioPlayerSelected()) {
3302+
return audioResolver.resolve(info);
3303+
}
3304+
3305+
if (isAudioOnly && videoResolver.getStreamSourceType().orElse(
3306+
SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY)
3307+
== SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY) {
3308+
// If the current info has only video streams with audio and if the stream is played as
3309+
// audio, we need to use the audio resolver, otherwise the video stream will be played
3310+
// in background.
3311+
return audioResolver.resolve(info);
3312+
}
3313+
3314+
// Even if the stream is played in background, we need to use the video resolver if the
3315+
// info played is separated video-only and audio-only streams; otherwise, if the audio
3316+
// resolver was called when the app was in background, the app will only stream audio when
3317+
// the user come back to the app and will never fetch the video stream.
3318+
// Note that the video is not fetched when the app is in background because the video
3319+
// renderer is fully disabled (see useVideoSource method), except for HLS streams
3320+
// (see https://github.com/google/ExoPlayer/issues/9282).
3321+
return videoResolver.resolve(info);
32963322
}
32973323

32983324
public void disablePreloadingOfCurrentTrack() {
@@ -4147,19 +4173,125 @@ public AppCompatActivity getParentActivity() {
41474173
return (AppCompatActivity) ((ViewGroup) binding.getRoot().getParent()).getContext();
41484174
}
41494175

4150-
private void useVideoSource(final boolean video) {
4151-
if (playQueue == null || isAudioOnly == !video || audioPlayerSelected()) {
4176+
private void useVideoSource(final boolean videoEnabled) {
4177+
if (playQueue == null || isAudioOnly == !videoEnabled || audioPlayerSelected()) {
41524178
return;
41534179
}
41544180

4155-
isAudioOnly = !video;
4156-
// When a user returns from background controls could be hidden
4157-
// but systemUI will be shown 100%. Hide it
4181+
isAudioOnly = !videoEnabled;
4182+
// When a user returns from background, controls could be hidden but SystemUI will be shown
4183+
// 100%. Hide it.
41584184
if (!isAudioOnly && !isControlsVisible()) {
41594185
hideSystemUIIfNeeded();
41604186
}
4187+
4188+
// The current metadata may be null sometimes (for e.g. when using an unstable connection
4189+
// in livestreams) so we will be not able to execute the block below.
4190+
// Reload the play queue manager in this case, which is the behavior when we don't know the
4191+
// index of the video renderer or playQueueManagerReloadingNeeded returns true.
4192+
if (currentMetadata == null) {
4193+
reloadPlayQueueManager();
4194+
setRecovery();
4195+
return;
4196+
}
4197+
4198+
final int videoRenderIndex = getVideoRendererIndex();
4199+
final StreamInfo info = currentMetadata.getMetadata();
4200+
4201+
// In the case we don't know the source type, fallback to the one with video with audio or
4202+
// audio-only source.
4203+
final SourceType sourceType = videoResolver.getStreamSourceType().orElse(
4204+
SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY);
4205+
4206+
if (playQueueManagerReloadingNeeded(sourceType, info, videoRenderIndex)) {
4207+
reloadPlayQueueManager();
4208+
} else {
4209+
final StreamType streamType = info.getStreamType();
4210+
if (streamType == StreamType.AUDIO_STREAM
4211+
|| streamType == StreamType.AUDIO_LIVE_STREAM) {
4212+
// Nothing to do more than setting the recovery position
4213+
setRecovery();
4214+
return;
4215+
}
4216+
4217+
final TrackGroupArray videoTrackGroupArray = Objects.requireNonNull(
4218+
trackSelector.getCurrentMappedTrackInfo()).getTrackGroups(videoRenderIndex);
4219+
if (videoEnabled) {
4220+
// Clearing the null selection override enable again the video stream (and its
4221+
// fetching).
4222+
trackSelector.setParameters(trackSelector.buildUponParameters()
4223+
.clearSelectionOverride(videoRenderIndex, videoTrackGroupArray));
4224+
} else {
4225+
// Using setRendererDisabled still fetch the video stream in background, contrary
4226+
// to setSelectionOverride with a null override.
4227+
trackSelector.setParameters(trackSelector.buildUponParameters()
4228+
.setSelectionOverride(videoRenderIndex, videoTrackGroupArray, null));
4229+
}
4230+
}
4231+
41614232
setRecovery();
4162-
reloadPlayQueueManager();
4233+
}
4234+
4235+
/**
4236+
* Return whether the play queue manager needs to be reloaded when switching player type.
4237+
*
4238+
* <p>
4239+
* The play queue manager needs to be reloaded if the video renderer index is not known and if
4240+
* the content is not an audio content, but also if none of the following cases is met:
4241+
*
4242+
* <ul>
4243+
* <li>the content is an {@link StreamType#AUDIO_STREAM audio stream} or an
4244+
* {@link StreamType#AUDIO_LIVE_STREAM audio live stream};</li>
4245+
* <li>the content is a {@link StreamType#LIVE_STREAM live stream} and the source type is a
4246+
* {@link SourceType#LIVE_STREAM live source};</li>
4247+
* <li>the content's source is {@link SourceType#VIDEO_WITH_SEPARATED_AUDIO a video stream
4248+
* with a separated audio source} or has no audio-only streams available <b>and</b> is a
4249+
* {@link StreamType#LIVE_STREAM live stream} or a
4250+
* {@link StreamType#LIVE_STREAM live stream}.
4251+
* </li>
4252+
* </ul>
4253+
* </p>
4254+
*
4255+
* @param sourceType the {@link SourceType} of the stream
4256+
* @param streamInfo the {@link StreamInfo} of the stream
4257+
* @param videoRendererIndex the video renderer index of the video source, if that's a video
4258+
* source (or {@link #RENDERER_UNAVAILABLE})
4259+
* @return whether the play queue manager needs to be reloaded
4260+
*/
4261+
private boolean playQueueManagerReloadingNeeded(final SourceType sourceType,
4262+
@NonNull final StreamInfo streamInfo,
4263+
final int videoRendererIndex) {
4264+
final StreamType streamType = streamInfo.getStreamType();
4265+
4266+
if (videoRendererIndex == RENDERER_UNAVAILABLE && streamType != StreamType.AUDIO_STREAM
4267+
&& streamType != StreamType.AUDIO_LIVE_STREAM) {
4268+
return true;
4269+
}
4270+
4271+
// The content is an audio stream, an audio live stream, or a live stream with a live
4272+
// source: it's not needed to reload the play queue manager because the stream source will
4273+
// be the same
4274+
if ((streamType == StreamType.AUDIO_STREAM || streamType == StreamType.AUDIO_LIVE_STREAM)
4275+
|| (streamType == StreamType.LIVE_STREAM
4276+
&& sourceType == SourceType.LIVE_STREAM)) {
4277+
return false;
4278+
}
4279+
4280+
// The content's source is a video with separated audio or a video with audio -> the video
4281+
// and its fetch may be disabled
4282+
// The content's source is a video with embedded audio and the content has no separated
4283+
// audio stream available: it's probably not needed to reload the play queue manager
4284+
// because the stream source will be probably the same as the current played
4285+
if (sourceType == SourceType.VIDEO_WITH_SEPARATED_AUDIO
4286+
|| (sourceType == SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY
4287+
&& isNullOrEmpty(streamInfo.getAudioStreams()))) {
4288+
// It's not needed to reload the play queue manager only if the content's stream type
4289+
// is a video stream or a live stream
4290+
return streamType != StreamType.VIDEO_STREAM && streamType != StreamType.LIVE_STREAM;
4291+
}
4292+
4293+
// Other cases: the play queue manager reload is needed
4294+
return true;
41634295
}
41644296
//endregion
41654297

@@ -4197,7 +4329,7 @@ private boolean isLoading() {
41974329
private boolean isLive() {
41984330
try {
41994331
return !exoPlayerIsNull() && simpleExoPlayer.isCurrentWindowDynamic();
4200-
} catch (@NonNull final IndexOutOfBoundsException e) {
4332+
} catch (final IndexOutOfBoundsException e) {
42014333
// Why would this even happen =(... but lets log it anyway, better safe than sorry
42024334
if (DEBUG) {
42034335
Log.d(TAG, "player.isCurrentWindowDynamic() failed: ", e);
@@ -4375,15 +4507,42 @@ private void setupVideoSurface() {
43754507
}
43764508

43774509
private void cleanupVideoSurface() {
4378-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // >=API23
4379-
if (surfaceHolderCallback != null) {
4380-
if (binding != null) {
4381-
binding.surfaceView.getHolder().removeCallback(surfaceHolderCallback);
4382-
}
4383-
surfaceHolderCallback.release();
4384-
surfaceHolderCallback = null;
4510+
// Only for API >= 23
4511+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && surfaceHolderCallback != null) {
4512+
if (binding != null) {
4513+
binding.surfaceView.getHolder().removeCallback(surfaceHolderCallback);
43854514
}
4515+
surfaceHolderCallback.release();
4516+
surfaceHolderCallback = null;
43864517
}
43874518
}
43884519
//endregion
4520+
4521+
/**
4522+
* Get the video renderer index of the current playing stream.
4523+
*
4524+
* This method returns the video renderer index of the current
4525+
* {@link MappingTrackSelector.MappedTrackInfo} or {@link #RENDERER_UNAVAILABLE} if the current
4526+
* {@link MappingTrackSelector.MappedTrackInfo} is null or if there is no video renderer index.
4527+
*
4528+
* @return the video renderer index or {@link #RENDERER_UNAVAILABLE} if it cannot be get
4529+
*/
4530+
private int getVideoRendererIndex() {
4531+
final MappingTrackSelector.MappedTrackInfo mappedTrackInfo = trackSelector
4532+
.getCurrentMappedTrackInfo();
4533+
4534+
if (mappedTrackInfo == null) {
4535+
return RENDERER_UNAVAILABLE;
4536+
}
4537+
4538+
// Check every renderer
4539+
return IntStream.range(0, mappedTrackInfo.getRendererCount())
4540+
// Check the renderer is a video renderer and has at least one track
4541+
.filter(i -> !mappedTrackInfo.getTrackGroups(i).isEmpty()
4542+
&& simpleExoPlayer.getRendererType(i) == C.TRACK_TYPE_VIDEO)
4543+
// Return the first index found (there is at most one renderer per renderer type)
4544+
.findFirst()
4545+
// No video renderer index with at least one track found: return unavailable index
4546+
.orElse(RENDERER_UNAVAILABLE);
4547+
}
43894548
}

0 commit comments

Comments
 (0)