diff --git a/app/src/main/java/org/schabi/newpipe/RouterActivity.java b/app/src/main/java/org/schabi/newpipe/RouterActivity.java index 9d6e44f04..adef3c0e4 100644 --- a/app/src/main/java/org/schabi/newpipe/RouterActivity.java +++ b/app/src/main/java/org/schabi/newpipe/RouterActivity.java @@ -633,7 +633,7 @@ public class RouterActivity extends AppCompatActivity { .subscribe(result -> { final List sortedVideoStreams = ListHelper .getSortedStreamVideosList(this, result.getVideoStreams(), - result.getVideoOnlyStreams(), false); + result.getVideoOnlyStreams(), false, false); final int selectedVideoStreamIndex = ListHelper .getDefaultResolutionIndex(this, sortedVideoStreams); diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index 5c954ad64..87bfbd12e 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -151,7 +151,7 @@ public class DownloadDialog extends DialogFragment public static DownloadDialog newInstance(final Context context, final StreamInfo info) { final ArrayList 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); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 78f0bfffb..0af5ec99e 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -1617,6 +1617,7 @@ public final class VideoDetailFragment activity, info.getVideoStreams(), info.getVideoOnlyStreams(), + false, false); selectedVideoStreamIndex = ListHelper .getDefaultResolutionIndex(activity, sortedVideoStreams); diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java index 87d2bd34a..85a50cb23 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -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 cues) { + public void onCues(@NonNull final List 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. + * + *

+ * 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: + * + *

    + *
  • the content is an {@link StreamType#AUDIO_STREAM audio stream} or an + * {@link StreamType#AUDIO_LIVE_STREAM audio live stream};
  • + *
  • the content is a {@link StreamType#LIVE_STREAM live stream} and the source type is a + * {@link SourceType#LIVE_STREAM live source};
  • + *
  • 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 and is a + * {@link StreamType#LIVE_STREAM live stream} or a + * {@link StreamType#LIVE_STREAM live stream}. + *
  • + *
+ *

+ * + * @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); + } } diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java index 245a85e71..11949f55d 100644 --- a/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java +++ b/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java @@ -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 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 getStreamSourceType() { + return Optional.ofNullable(streamSourceType); + } + @Nullable public String getPlaybackQuality() { return playbackQuality; diff --git a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java index eb3c21827..c3ccef87c 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java @@ -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 AUDIO_FORMAT_EFFICIENCY_RANKING = Arrays.asList(MediaFormat.WEBMA, MediaFormat.M4A, MediaFormat.MP3); - private static final List HIGH_RESOLUTION_LIST - = Arrays.asList("1440p", "2160p", "1440p60", "2160p60"); + private static final Set 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 getSortedStreamVideosList(final Context context, - final List videoStreams, - final List - videoOnlyStreams, - final boolean ascendingOrder) { + @NonNull + public static List getSortedStreamVideosList( + @NonNull final Context context, + @Nullable final List videoStreams, + @Nullable final List 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 getSortedStreamVideosList(final MediaFormat defaultFormat, - final boolean showHigherResolutions, - final List videoStreams, - final List videoOnlyStreams, - final boolean ascendingOrder) { - final ArrayList retList = new ArrayList<>(); + @NonNull + static List getSortedStreamVideosList( + @Nullable final MediaFormat defaultFormat, + final boolean showHigherResolutions, + @Nullable final List videoStreams, + @Nullable final List videoOnlyStreams, + final boolean ascendingOrder, + final boolean preferVideoOnlyStreams + ) { + // Determine order of streams + // The last added list is preferred + final List> videoStreamsOrdered = + preferVideoOnlyStreams + ? Arrays.asList(videoStreams, videoOnlyStreams) + : Arrays.asList(videoOnlyStreams, videoStreams); + + final List 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 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 *
- * ascendingOrder ? 360 < 720 < 721 < 1080 < 1081 - * !ascendingOrder ? 1081 < 1080 < 721 < 720 < 360 + * ascendingOrder ? 360 < 720 < 721 < 1080 < 1081 + * !ascendingOrder ? 1081 < 1080 < 721 < 720 < 360 * * @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 videoStreams, - final boolean ascendingOrder) { + private static List sortStreamList(final List videoStreams, + final boolean ascendingOrder) { final Comparator 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 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 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 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 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 audioStreams, + final Comparator 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); } /** diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index 22e0a2dd0..49ee49668 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -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 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 { diff --git a/app/src/test/java/org/schabi/newpipe/util/ListHelperTest.java b/app/src/test/java/org/schabi/newpipe/util/ListHelperTest.java index d126f8473..f72d08c13 100644 --- a/app/src/test/java/org/schabi/newpipe/util/ListHelperTest.java +++ b/app/src/test/java/org/schabi/newpipe/util/ListHelperTest.java @@ -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 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 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 result = ListHelper.getSortedStreamVideosList(MediaFormat.MPEG_4, true, + null, VIDEO_ONLY_STREAMS_TEST_LIST, true, true); + + List 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 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 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 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); } } diff --git a/checkstyle-suppressions.xml b/checkstyle-suppressions.xml index f7ed38bdc..3c5e4891a 100644 --- a/checkstyle-suppressions.xml +++ b/checkstyle-suppressions.xml @@ -8,8 +8,8 @@ lines="232,304"/> + files="InfoListAdapter.java" + lines="253,325"/>