mirror of
				https://github.com/TeamNewPipe/NewPipe
				synced 2025-10-30 23:03:00 +00:00 
			
		
		
		
	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:
		| @@ -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" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 litetex
					litetex