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
a95318a4f9
|
@ -633,7 +633,7 @@ public class RouterActivity extends AppCompatActivity {
|
|||
.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);
|
||||
|
||||
|
|
|
@ -151,7 +151,7 @@ public class DownloadDialog extends DialogFragment
|
|||
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);
|
||||
|
|
|
@ -1617,6 +1617,7 @@ public final class VideoDetailFragment
|
|||
activity,
|
||||
info.getVideoStreams(),
|
||||
info.getVideoOnlyStreams(),
|
||||
false,
|
||||
false);
|
||||
selectedVideoStreamIndex = ListHelper
|
||||
.getDefaultResolutionIndex(activity, sortedVideoStreams);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.content.Context;
|
|||
import android.content.SharedPreferences;
|
||||
import android.net.ConnectivityManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
@ -19,7 +20,11 @@ import java.util.Arrays;
|
|||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public final class ListHelper {
|
||||
// Video format in order of quality. 0=lowest quality, n=highest quality
|
||||
|
@ -33,8 +38,9 @@ public final class ListHelper {
|
|||
private static final List<MediaFormat> AUDIO_FORMAT_EFFICIENCY_RANKING =
|
||||
Arrays.asList(MediaFormat.WEBMA, MediaFormat.M4A, MediaFormat.MP3);
|
||||
|
||||
private static final List<String> HIGH_RESOLUTION_LIST
|
||||
= Arrays.asList("1440p", "2160p", "1440p60", "2160p60");
|
||||
private static final Set<String> HIGH_RESOLUTION_LIST
|
||||
// Uses a HashSet for better performance
|
||||
= new HashSet<>(Arrays.asList("1440p", "2160p", "1440p60", "2160p60"));
|
||||
|
||||
private ListHelper() { }
|
||||
|
||||
|
@ -108,17 +114,21 @@ public final class ListHelper {
|
|||
* Join the two lists of video streams (video_only and normal videos),
|
||||
* and sort them according with default format chosen by the user.
|
||||
*
|
||||
* @param context context to search for the format to give preference
|
||||
* @param videoStreams normal videos list
|
||||
* @param videoOnlyStreams video only stream list
|
||||
* @param ascendingOrder true -> smallest to greatest | false -> greatest to smallest
|
||||
* @param context the context to search for the format to give preference
|
||||
* @param videoStreams the normal videos list
|
||||
* @param videoOnlyStreams the video-only stream list
|
||||
* @param ascendingOrder true -> smallest to greatest | false -> greatest to smallest
|
||||
* @param preferVideoOnlyStreams if video-only streams should preferred when both video-only
|
||||
* streams and normal video streams are available
|
||||
* @return the sorted list
|
||||
*/
|
||||
public static List<VideoStream> getSortedStreamVideosList(final Context context,
|
||||
final List<VideoStream> videoStreams,
|
||||
final List<VideoStream>
|
||||
videoOnlyStreams,
|
||||
final boolean ascendingOrder) {
|
||||
@NonNull
|
||||
public static List<VideoStream> getSortedStreamVideosList(
|
||||
@NonNull final Context context,
|
||||
@Nullable final List<VideoStream> videoStreams,
|
||||
@Nullable final List<VideoStream> videoOnlyStreams,
|
||||
final boolean ascendingOrder,
|
||||
final boolean preferVideoOnlyStreams) {
|
||||
final SharedPreferences preferences
|
||||
= PreferenceManager.getDefaultSharedPreferences(context);
|
||||
|
||||
|
@ -128,7 +138,7 @@ public final class ListHelper {
|
|||
R.string.default_video_format_key, R.string.default_video_format_value);
|
||||
|
||||
return getSortedStreamVideosList(defaultFormat, showHigherResolutions, videoStreams,
|
||||
videoOnlyStreams, ascendingOrder);
|
||||
videoOnlyStreams, ascendingOrder, preferVideoOnlyStreams);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
|
@ -192,56 +202,55 @@ public final class ListHelper {
|
|||
* Join the two lists of video streams (video_only and normal videos),
|
||||
* and sort them according with default format chosen by the user.
|
||||
*
|
||||
* @param defaultFormat format to give preference
|
||||
* @param showHigherResolutions show >1080p resolutions
|
||||
* @param videoStreams normal videos list
|
||||
* @param videoOnlyStreams video only stream list
|
||||
* @param ascendingOrder true -> smallest to greatest | false -> greatest to smallest
|
||||
* @param defaultFormat format to give preference
|
||||
* @param showHigherResolutions show >1080p resolutions
|
||||
* @param videoStreams normal videos list
|
||||
* @param videoOnlyStreams video only stream list
|
||||
* @param ascendingOrder true -> smallest to greatest | false -> greatest to smallest
|
||||
* @param preferVideoOnlyStreams if video-only streams should preferred when both video-only
|
||||
* streams and normal video streams are available
|
||||
* @return the sorted list
|
||||
*/
|
||||
static List<VideoStream> getSortedStreamVideosList(final MediaFormat defaultFormat,
|
||||
final boolean showHigherResolutions,
|
||||
final List<VideoStream> videoStreams,
|
||||
final List<VideoStream> videoOnlyStreams,
|
||||
final boolean ascendingOrder) {
|
||||
final ArrayList<VideoStream> retList = new ArrayList<>();
|
||||
@NonNull
|
||||
static List<VideoStream> getSortedStreamVideosList(
|
||||
@Nullable final MediaFormat defaultFormat,
|
||||
final boolean showHigherResolutions,
|
||||
@Nullable final List<VideoStream> videoStreams,
|
||||
@Nullable final List<VideoStream> videoOnlyStreams,
|
||||
final boolean ascendingOrder,
|
||||
final boolean preferVideoOnlyStreams
|
||||
) {
|
||||
// Determine order of streams
|
||||
// The last added list is preferred
|
||||
final List<List<VideoStream>> videoStreamsOrdered =
|
||||
preferVideoOnlyStreams
|
||||
? Arrays.asList(videoStreams, videoOnlyStreams)
|
||||
: Arrays.asList(videoOnlyStreams, videoStreams);
|
||||
|
||||
final List<VideoStream> allInitialStreams = videoStreamsOrdered.stream()
|
||||
// Ignore lists that are null
|
||||
.filter(Objects::nonNull)
|
||||
.flatMap(List::stream)
|
||||
// Filter out higher resolutions (or not if high resolutions should always be shown)
|
||||
.filter(stream -> showHigherResolutions
|
||||
|| !HIGH_RESOLUTION_LIST.contains(stream.getResolution()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
final HashMap<String, VideoStream> hashMap = new HashMap<>();
|
||||
|
||||
if (videoOnlyStreams != null) {
|
||||
for (final VideoStream stream : videoOnlyStreams) {
|
||||
if (!showHigherResolutions
|
||||
&& HIGH_RESOLUTION_LIST.contains(stream.getResolution())) {
|
||||
continue;
|
||||
}
|
||||
retList.add(stream);
|
||||
}
|
||||
}
|
||||
if (videoStreams != null) {
|
||||
for (final VideoStream stream : videoStreams) {
|
||||
if (!showHigherResolutions
|
||||
&& HIGH_RESOLUTION_LIST.contains(stream.getResolution())) {
|
||||
continue;
|
||||
}
|
||||
retList.add(stream);
|
||||
}
|
||||
}
|
||||
|
||||
// Add all to the hashmap
|
||||
for (final VideoStream videoStream : retList) {
|
||||
for (final VideoStream videoStream : allInitialStreams) {
|
||||
hashMap.put(videoStream.getResolution(), videoStream);
|
||||
}
|
||||
|
||||
// Override the values when the key == resolution, with the defaultFormat
|
||||
for (final VideoStream videoStream : retList) {
|
||||
for (final VideoStream videoStream : allInitialStreams) {
|
||||
if (videoStream.getFormat() == defaultFormat) {
|
||||
hashMap.put(videoStream.getResolution(), videoStream);
|
||||
}
|
||||
}
|
||||
|
||||
retList.clear();
|
||||
retList.addAll(hashMap.values());
|
||||
sortStreamList(retList, ascendingOrder);
|
||||
return retList;
|
||||
// Return the sorted list
|
||||
return sortStreamList(new ArrayList<>(hashMap.values()), ascendingOrder);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -257,16 +266,18 @@ public final class ListHelper {
|
|||
* 1080p -> 1080
|
||||
* 1080p60 -> 1081
|
||||
* <br>
|
||||
* ascendingOrder ? 360 < 720 < 721 < 1080 < 1081
|
||||
* !ascendingOrder ? 1081 < 1080 < 721 < 720 < 360</pre></blockquote>
|
||||
* ascendingOrder ? 360 < 720 < 721 < 1080 < 1081
|
||||
* !ascendingOrder ? 1081 < 1080 < 721 < 720 < 360</pre></blockquote>
|
||||
*
|
||||
* @param videoStreams list that the sorting will be applied
|
||||
* @param ascendingOrder true -> smallest to greatest | false -> greatest to smallest
|
||||
* @return The sorted list (same reference as parameter videoStreams)
|
||||
*/
|
||||
private static void sortStreamList(final List<VideoStream> videoStreams,
|
||||
final boolean ascendingOrder) {
|
||||
private static List<VideoStream> sortStreamList(final List<VideoStream> videoStreams,
|
||||
final boolean ascendingOrder) {
|
||||
final Comparator<VideoStream> comparator = ListHelper::compareVideoStreamResolution;
|
||||
Collections.sort(videoStreams, ascendingOrder ? comparator : comparator.reversed());
|
||||
return videoStreams;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -277,28 +288,12 @@ public final class ListHelper {
|
|||
* @param audioStreams List of audio streams
|
||||
* @return Index of audio stream that produces the most compact results or -1 if not found
|
||||
*/
|
||||
static int getHighestQualityAudioIndex(@Nullable MediaFormat format,
|
||||
final List<AudioStream> audioStreams) {
|
||||
int result = -1;
|
||||
if (audioStreams != null) {
|
||||
while (result == -1) {
|
||||
AudioStream prevStream = null;
|
||||
for (int idx = 0; idx < audioStreams.size(); idx++) {
|
||||
final AudioStream stream = audioStreams.get(idx);
|
||||
if ((format == null || stream.getFormat() == format)
|
||||
&& (prevStream == null || compareAudioStreamBitrate(prevStream, stream,
|
||||
AUDIO_FORMAT_QUALITY_RANKING) < 0)) {
|
||||
prevStream = stream;
|
||||
result = idx;
|
||||
}
|
||||
}
|
||||
if (result == -1 && format == null) {
|
||||
break;
|
||||
}
|
||||
format = null;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
static int getHighestQualityAudioIndex(@Nullable final MediaFormat format,
|
||||
@Nullable final List<AudioStream> audioStreams) {
|
||||
return getAudioIndexByHighestRank(format, audioStreams,
|
||||
// Compares descending (last = highest rank)
|
||||
(s1, s2) -> compareAudioStreamBitrate(s1, s2, AUDIO_FORMAT_QUALITY_RANKING)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -309,28 +304,47 @@ public final class ListHelper {
|
|||
* @param audioStreams List of audio streams
|
||||
* @return Index of audio stream that produces the most compact results or -1 if not found
|
||||
*/
|
||||
static int getMostCompactAudioIndex(@Nullable MediaFormat format,
|
||||
final List<AudioStream> audioStreams) {
|
||||
int result = -1;
|
||||
if (audioStreams != null) {
|
||||
while (result == -1) {
|
||||
AudioStream prevStream = null;
|
||||
for (int idx = 0; idx < audioStreams.size(); idx++) {
|
||||
final AudioStream stream = audioStreams.get(idx);
|
||||
if ((format == null || stream.getFormat() == format)
|
||||
&& (prevStream == null || compareAudioStreamBitrate(prevStream, stream,
|
||||
AUDIO_FORMAT_EFFICIENCY_RANKING) > 0)) {
|
||||
prevStream = stream;
|
||||
result = idx;
|
||||
}
|
||||
}
|
||||
if (result == -1 && format == null) {
|
||||
break;
|
||||
}
|
||||
format = null;
|
||||
}
|
||||
static int getMostCompactAudioIndex(@Nullable final MediaFormat format,
|
||||
@Nullable final List<AudioStream> audioStreams) {
|
||||
|
||||
return getAudioIndexByHighestRank(format, audioStreams,
|
||||
// The "-" is important -> Compares ascending (first = highest rank)
|
||||
(s1, s2) -> -compareAudioStreamBitrate(s1, s2, AUDIO_FORMAT_EFFICIENCY_RANKING)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the audio-stream from the list with the highest rank, depending on the comparator.
|
||||
* Format will be ignored if it yields no results.
|
||||
*
|
||||
* @param targetedFormat The target format type or null if it doesn't matter
|
||||
* @param audioStreams List of audio streams
|
||||
* @param comparator The comparator used for determining the max/best/highest ranked value
|
||||
* @return Index of audio stream that produces the highest ranked result or -1 if not found
|
||||
*/
|
||||
private static int getAudioIndexByHighestRank(@Nullable final MediaFormat targetedFormat,
|
||||
@Nullable final List<AudioStream> audioStreams,
|
||||
final Comparator<AudioStream> comparator) {
|
||||
if (audioStreams == null || audioStreams.isEmpty()) {
|
||||
return -1;
|
||||
}
|
||||
return result;
|
||||
|
||||
final AudioStream highestRankedAudioStream = audioStreams.stream()
|
||||
.filter(audioStream -> targetedFormat == null
|
||||
|| audioStream.getFormat() == targetedFormat)
|
||||
.max(comparator)
|
||||
.orElse(null);
|
||||
|
||||
if (highestRankedAudioStream == null) {
|
||||
// Fallback: Ignore targetedFormat if not null
|
||||
if (targetedFormat != null) {
|
||||
return getAudioIndexByHighestRank(null, audioStreams, comparator);
|
||||
}
|
||||
// targetedFormat is already null -> return -1
|
||||
return -1;
|
||||
}
|
||||
|
||||
return audioStreams.indexOf(highestRankedAudioStream);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -214,7 +214,8 @@ public final class NavigationHelper {
|
|||
// External Players
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public static void playOnExternalAudioPlayer(final Context context, final StreamInfo info) {
|
||||
public static void playOnExternalAudioPlayer(@NonNull final Context context,
|
||||
@NonNull final StreamInfo info) {
|
||||
final int index = ListHelper.getDefaultAudioFormat(context, info.getAudioStreams());
|
||||
|
||||
if (index == -1) {
|
||||
|
@ -226,9 +227,11 @@ public final class NavigationHelper {
|
|||
playOnExternalPlayer(context, info.getName(), info.getUploaderName(), audioStream);
|
||||
}
|
||||
|
||||
public static void playOnExternalVideoPlayer(final Context context, final StreamInfo info) {
|
||||
public static void playOnExternalVideoPlayer(@NonNull final Context context,
|
||||
@NonNull final StreamInfo info) {
|
||||
final ArrayList<VideoStream> videoStreamsList = new ArrayList<>(
|
||||
ListHelper.getSortedStreamVideosList(context, info.getVideoStreams(), null, false));
|
||||
ListHelper.getSortedStreamVideosList(context, info.getVideoStreams(), null, false,
|
||||
false));
|
||||
final int index = ListHelper.getDefaultResolutionIndex(context, videoStreamsList);
|
||||
|
||||
if (index == -1) {
|
||||
|
@ -240,8 +243,10 @@ public final class NavigationHelper {
|
|||
playOnExternalPlayer(context, info.getName(), info.getUploaderName(), videoStream);
|
||||
}
|
||||
|
||||
public static void playOnExternalPlayer(final Context context, final String name,
|
||||
final String artist, final Stream stream) {
|
||||
public static void playOnExternalPlayer(@NonNull final Context context,
|
||||
@Nullable final String name,
|
||||
@Nullable final String artist,
|
||||
@NonNull final Stream stream) {
|
||||
final Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.setDataAndType(Uri.parse(stream.getUrl()), stream.getFormat().getMimeType());
|
||||
|
@ -253,7 +258,8 @@ public final class NavigationHelper {
|
|||
resolveActivityOrAskToInstall(context, intent);
|
||||
}
|
||||
|
||||
public static void resolveActivityOrAskToInstall(final Context context, final Intent intent) {
|
||||
public static void resolveActivityOrAskToInstall(@NonNull final Context context,
|
||||
@NonNull final Intent intent) {
|
||||
if (intent.resolveActivity(context.getPackageManager()) != null) {
|
||||
ShareUtils.openIntentInApp(context, intent, false);
|
||||
} else {
|
||||
|
|
|
@ -10,6 +10,8 @@ import java.util.Arrays;
|
|||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class ListHelperTest {
|
||||
private static final String BEST_RESOLUTION_KEY = "best_resolution";
|
||||
|
@ -47,19 +49,14 @@ public class ListHelperTest {
|
|||
@Test
|
||||
public void getSortedStreamVideosListTest() {
|
||||
List<VideoStream> result = ListHelper.getSortedStreamVideosList(MediaFormat.MPEG_4, true,
|
||||
VIDEO_STREAMS_TEST_LIST, VIDEO_ONLY_STREAMS_TEST_LIST, true);
|
||||
VIDEO_STREAMS_TEST_LIST, VIDEO_ONLY_STREAMS_TEST_LIST, true, false);
|
||||
|
||||
List<String> expected = Arrays.asList("144p", "240p", "360p", "480p", "720p", "720p60",
|
||||
"1080p", "1080p60", "1440p60", "2160p", "2160p60");
|
||||
// for (VideoStream videoStream : result) {
|
||||
// System.out.println(videoStream.resolution + " > "
|
||||
// + MediaFormat.getSuffixById(videoStream.format) + " > "
|
||||
// + videoStream.isVideoOnly);
|
||||
// }
|
||||
|
||||
assertEquals(result.size(), expected.size());
|
||||
assertEquals(expected.size(), result.size());
|
||||
for (int i = 0; i < result.size(); i++) {
|
||||
assertEquals(result.get(i).resolution, expected.get(i));
|
||||
assertEquals(expected.get(i), result.get(i).resolution);
|
||||
}
|
||||
|
||||
////////////////////
|
||||
|
@ -67,12 +64,59 @@ public class ListHelperTest {
|
|||
//////////////////
|
||||
|
||||
result = ListHelper.getSortedStreamVideosList(MediaFormat.MPEG_4, true,
|
||||
VIDEO_STREAMS_TEST_LIST, VIDEO_ONLY_STREAMS_TEST_LIST, false);
|
||||
VIDEO_STREAMS_TEST_LIST, VIDEO_ONLY_STREAMS_TEST_LIST, false, false);
|
||||
expected = Arrays.asList("2160p60", "2160p", "1440p60", "1080p60", "1080p", "720p60",
|
||||
"720p", "480p", "360p", "240p", "144p");
|
||||
assertEquals(result.size(), expected.size());
|
||||
assertEquals(expected.size(), result.size());
|
||||
for (int i = 0; i < result.size(); i++) {
|
||||
assertEquals(result.get(i).resolution, expected.get(i));
|
||||
assertEquals(expected.get(i), result.get(i).resolution);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getSortedStreamVideosListWithPreferVideoOnlyStreamsTest() {
|
||||
List<VideoStream> result = ListHelper.getSortedStreamVideosList(MediaFormat.MPEG_4, true,
|
||||
null, VIDEO_ONLY_STREAMS_TEST_LIST, true, true);
|
||||
|
||||
List<String> expected =
|
||||
Arrays.asList("720p", "720p60", "1080p", "1080p60", "1440p60", "2160p", "2160p60");
|
||||
|
||||
assertEquals(expected.size(), result.size());
|
||||
for (int i = 0; i < result.size(); i++) {
|
||||
assertEquals(expected.get(i), result.get(i).resolution);
|
||||
assertTrue(result.get(i).isVideoOnly);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////
|
||||
// No video only streams -> should return mixed streams //
|
||||
//////////////////////////////////////////////////////////
|
||||
|
||||
result = ListHelper.getSortedStreamVideosList(MediaFormat.MPEG_4, true,
|
||||
VIDEO_STREAMS_TEST_LIST, null, false, true);
|
||||
expected = Arrays.asList("720p", "480p", "360p", "240p", "144p");
|
||||
assertEquals(expected.size(), result.size());
|
||||
for (int i = 0; i < result.size(); i++) {
|
||||
assertEquals(expected.get(i), result.get(i).resolution);
|
||||
assertFalse(result.get(i).isVideoOnly);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////
|
||||
// Both types of streams -> should return correct one streams //
|
||||
/////////////////////////////////////////////////////////////////
|
||||
|
||||
result = ListHelper.getSortedStreamVideosList(MediaFormat.MPEG_4, true,
|
||||
VIDEO_STREAMS_TEST_LIST, VIDEO_ONLY_STREAMS_TEST_LIST, true, true);
|
||||
expected = Arrays.asList("144p", "240p", "360p", "480p", "720p", "720p60",
|
||||
"1080p", "1080p60", "1440p60", "2160p", "2160p60");
|
||||
final List<String> expectedVideoOnly =
|
||||
Arrays.asList("720p", "720p60", "1080p", "1080p60", "1440p60", "2160p", "2160p60");
|
||||
|
||||
assertEquals(expected.size(), result.size());
|
||||
for (int i = 0; i < result.size(); i++) {
|
||||
assertEquals(expected.get(i), result.get(i).resolution);
|
||||
assertEquals(
|
||||
expectedVideoOnly.contains(result.get(i).resolution),
|
||||
result.get(i).isVideoOnly);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -83,12 +127,12 @@ public class ListHelperTest {
|
|||
//////////////////////////////////
|
||||
|
||||
final List<VideoStream> result = ListHelper.getSortedStreamVideosList(MediaFormat.MPEG_4,
|
||||
false, VIDEO_STREAMS_TEST_LIST, VIDEO_ONLY_STREAMS_TEST_LIST, false);
|
||||
false, VIDEO_STREAMS_TEST_LIST, VIDEO_ONLY_STREAMS_TEST_LIST, false, false);
|
||||
final List<String> expected = Arrays.asList(
|
||||
"1080p60", "1080p", "720p60", "720p", "480p", "360p", "240p", "144p");
|
||||
assertEquals(result.size(), expected.size());
|
||||
assertEquals(expected.size(), result.size());
|
||||
for (int i = 0; i < result.size(); i++) {
|
||||
assertEquals(result.get(i).resolution, expected.get(i));
|
||||
assertEquals(expected.get(i), result.get(i).resolution);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,8 +8,8 @@
|
|||
lines="232,304"/>
|
||||
|
||||
<suppress checks="FinalParameters"
|
||||
files="ListHelper.java"
|
||||
lines="280,312"/>
|
||||
files="InfoListAdapter.java"
|
||||
lines="253,325"/>
|
||||
|
||||
<suppress checks="EmptyBlock"
|
||||
files="ContentSettingsFragment.java"
|
||||
|
|
Loading…
Reference in New Issue