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)
This commit is contained in:
commit
ae111a8850
9 changed files with 393 additions and 147 deletions
|
|
@ -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;
|
||||
|
|
@ -175,6 +178,7 @@ 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;
|
||||
import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHolder;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
|
|
@ -193,6 +197,7 @@ import java.util.ArrayList;
|
|||
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;
|
||||
|
|
@ -2449,9 +2454,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 + "]");
|
||||
|
|
@ -2499,7 +2504,7 @@ public final class Player implements
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onCues(final List<Cue> cues) {
|
||||
public void onCues(@NonNull final List<Cue> cues) {
|
||||
binding.subtitleView.onCues(cues);
|
||||
}
|
||||
//endregion
|
||||
|
|
@ -3005,18 +3010,19 @@ 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 || 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) {
|
||||
|
|
@ -3292,7 +3298,27 @@ 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);
|
||||
}
|
||||
|
||||
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() {
|
||||
|
|
@ -4147,19 +4173,125 @@ 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;
|
||||
// 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.
|
||||
*
|
||||
* <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
|
||||
|
||||
|
|
@ -4197,7 +4329,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);
|
||||
|
|
@ -4375,15 +4507,42 @@ 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
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import org.schabi.newpipe.util.ListHelper;
|
|||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static com.google.android.exoplayer2.C.TIME_UNSET;
|
||||
|
||||
|
|
@ -31,10 +32,17 @@ public class VideoPlaybackResolver implements PlaybackResolver {
|
|||
private final PlayerDataSource dataSource;
|
||||
@NonNull
|
||||
private final QualityResolver qualityResolver;
|
||||
private SourceType streamSourceType;
|
||||
|
||||
@Nullable
|
||||
private String playbackQuality;
|
||||
|
||||
public enum SourceType {
|
||||
LIVE_STREAM,
|
||||
VIDEO_WITH_SEPARATED_AUDIO,
|
||||
VIDEO_WITH_AUDIO_OR_AUDIO_ONLY
|
||||
}
|
||||
|
||||
public VideoPlaybackResolver(@NonNull final Context context,
|
||||
@NonNull final PlayerDataSource dataSource,
|
||||
@NonNull final QualityResolver qualityResolver) {
|
||||
|
|
@ -48,6 +56,7 @@ public class VideoPlaybackResolver implements PlaybackResolver {
|
|||
public MediaSource resolve(@NonNull final StreamInfo info) {
|
||||
final MediaSource liveSource = maybeBuildLiveMediaSource(dataSource, info);
|
||||
if (liveSource != null) {
|
||||
streamSourceType = SourceType.LIVE_STREAM;
|
||||
return liveSource;
|
||||
}
|
||||
|
||||
|
|
@ -55,7 +64,7 @@ public class VideoPlaybackResolver implements PlaybackResolver {
|
|||
|
||||
// Create video stream source
|
||||
final List<VideoStream> videos = ListHelper.getSortedStreamVideosList(context,
|
||||
info.getVideoStreams(), info.getVideoOnlyStreams(), false);
|
||||
info.getVideoStreams(), info.getVideoOnlyStreams(), false, true);
|
||||
final int index;
|
||||
if (videos.isEmpty()) {
|
||||
index = -1;
|
||||
|
|
@ -85,6 +94,9 @@ public class VideoPlaybackResolver implements PlaybackResolver {
|
|||
PlayerHelper.cacheKeyOf(info, audio),
|
||||
MediaFormat.getSuffixById(audio.getFormatId()), tag);
|
||||
mediaSources.add(audioSource);
|
||||
streamSourceType = SourceType.VIDEO_WITH_SEPARATED_AUDIO;
|
||||
} else {
|
||||
streamSourceType = SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY;
|
||||
}
|
||||
|
||||
// If there is no audio or video sources, then this media source cannot be played back
|
||||
|
|
@ -118,6 +130,16 @@ public class VideoPlaybackResolver implements PlaybackResolver {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last resolved {@link StreamInfo}'s {@link SourceType source type}.
|
||||
*
|
||||
* @return {@link Optional#empty()} if nothing was resolved, otherwise the {@link SourceType}
|
||||
* of the last resolved {@link StreamInfo} inside an {@link Optional}
|
||||
*/
|
||||
public Optional<SourceType> getStreamSourceType() {
|
||||
return Optional.ofNullable(streamSourceType);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getPlaybackQuality() {
|
||||
return playbackQuality;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue