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:
litetex 2022-02-26 16:16:18 +01:00 committed by GitHub
commit a95318a4f9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 393 additions and 147 deletions

View file

@ -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);

View file

@ -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);

View file

@ -1617,6 +1617,7 @@ public final class VideoDetailFragment
activity,
info.getVideoStreams(),
info.getVideoOnlyStreams(),
false,
false);
selectedVideoStreamIndex = ListHelper
.getDefaultResolutionIndex(activity, sortedVideoStreams);

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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);
}
/**

View file

@ -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 {

View file

@ -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);
}
}

View file

@ -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"