kopia lustrzana https://github.com/TeamNewPipe/NewPipe
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.pull/7349/head
rodzic
af80d96b9e
commit
336f9f3813
|
@ -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<Cue> cues) {
|
||||
public void onCues(@NonNull final List<Cue> 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,4 +6,6 @@ import androidx.annotation.Nullable;
|
|||
public interface Resolver<Source, Product> {
|
||||
@Nullable
|
||||
Product resolve(@NonNull Source source);
|
||||
|
||||
boolean isVideoStreamVideoOnly();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
Ładowanie…
Reference in New Issue