From 336f9f3813837746a5642a21f92966dda68bbf0a Mon Sep 17 00:00:00 2001
From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com>
Date: Wed, 27 Oct 2021 20:28:53 +0200
Subject: [PATCH 01/16] Add seamless transition between background player and
video players (for video-only and audio-only streams only)
This is only available when playing video-only streams (and when there is no audio stream and only video streams with audio) and audio-only streams.
For more details about which conditions are required to get this transition, look at the changes in the useVideoSource(boolean) method of the Player class.
---
.../org/schabi/newpipe/player/Player.java | 115 +++++++++++++++---
.../resolver/AudioPlaybackResolver.java | 5 +
.../newpipe/player/resolver/Resolver.java | 2 +
.../resolver/VideoPlaybackResolver.java | 9 ++
4 files changed, 112 insertions(+), 19 deletions(-)
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 e0debc4e7..bec120baa 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;
@@ -2443,9 +2446,9 @@ public final class Player implements
}
@Override
- public void onPositionDiscontinuity(
- final PositionInfo oldPosition, final PositionInfo newPosition,
- @DiscontinuityReason final int discontinuityReason) {
+ public void onPositionDiscontinuity(@NonNull final PositionInfo oldPosition,
+ @NonNull final PositionInfo newPosition,
+ @DiscontinuityReason final int discontinuityReason) {
if (DEBUG) {
Log.d(TAG, "ExoPlayer - onPositionDiscontinuity() called with "
+ "discontinuityReason = [" + discontinuityReason + "]");
@@ -2493,7 +2496,7 @@ public final class Player implements
}
@Override
- public void onCues(final List cues) {
+ public void onCues(@NonNull final List cues) {
binding.subtitleView.onCues(cues);
}
//endregion
@@ -2999,8 +3002,16 @@ public final class Player implements
final MediaSourceTag metadata;
try {
- metadata = (MediaSourceTag) simpleExoPlayer.getCurrentTag();
- } catch (IndexOutOfBoundsException | ClassCastException error) {
+ final MediaItem currentMediaItem = simpleExoPlayer.getCurrentMediaItem();
+ if (currentMediaItem != null) {
+ final MediaItem.PlaybackProperties playbackProperties =
+ currentMediaItem.playbackProperties;
+ metadata = (MediaSourceTag) (playbackProperties != null ? playbackProperties.tag
+ : null);
+ } else {
+ metadata = null;
+ }
+ } catch (final IndexOutOfBoundsException | ClassCastException error) {
if (DEBUG) {
Log.d(TAG, "Could not update metadata: " + error.getMessage());
error.printStackTrace();
@@ -3286,7 +3297,15 @@ public final class Player implements
@Override // own playback listener
@Nullable
public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) {
- return (isAudioOnly ? audioResolver : videoResolver).resolve(info);
+ if (audioPlayerSelected()) {
+ return audioResolver.resolve(info);
+ } else {
+ if (isAudioOnly && !videoResolver.isVideoStreamVideoOnly()) {
+ return audioResolver.resolve(info);
+ }
+
+ return videoResolver.resolve(info);
+ }
}
public void disablePreloadingOfCurrentTrack() {
@@ -4141,19 +4160,61 @@ public final class Player implements
return (AppCompatActivity) ((ViewGroup) binding.getRoot().getParent()).getContext();
}
- private void useVideoSource(final boolean video) {
- if (playQueue == null || isAudioOnly == !video || audioPlayerSelected()) {
+ private void useVideoSource(final boolean videoEnabled) {
+ if (playQueue == null || isAudioOnly == !videoEnabled || audioPlayerSelected()) {
return;
}
- isAudioOnly = !video;
+ isAudioOnly = !videoEnabled;
// When a user returns from background controls could be hidden
// but systemUI will be shown 100%. Hide it
if (!isAudioOnly && !isControlsVisible()) {
hideSystemUIIfNeeded();
}
+
+ final int videoRenderIndex = getVideoRendererIndex();
+
+ // We can safely assume that currentMetadata is not null (otherwise this method isn't
+ // called) so we can use the requireNonNull method of the Objects class.
+ final StreamInfo info = Objects.requireNonNull(currentMetadata).getMetadata();
+
+ /* For video streams: we don't want to stream in background the video stream so if the
+ video stream played is not a video-only stream and if there is an audio stream available,
+ play this audio stream in background by reloading the play queue manager.
+ Otherwise the video renderer will be just disabled (because there is no
+ other stream for it to play the audio): if the video stream is video-only, only the audio
+ stream will be fetched and the video stream will be fetched again when the user return to a
+ video player.
+
+ For audio streams: nothing is done, it's not needed to reload the player with the same
+ audio stream.
+
+ In the case where we don't know the index of the video renderer, the play queue manager
+ is also reloaded. */
+
+ final StreamType streamType = info.getStreamType();
+
+ final boolean isVideoStreamTypeAndIsVideoOnlyStreamOrNoAudioStreamsAvailable =
+ (streamType == StreamType.VIDEO_STREAM || streamType == StreamType.LIVE_STREAM)
+ && (videoResolver.isVideoStreamVideoOnly()
+ || isNullOrEmpty(info.getAudioStreams()));
+ if (videoRenderIndex != RENDERER_UNAVAILABLE
+ && isVideoStreamTypeAndIsVideoOnlyStreamOrNoAudioStreamsAvailable) {
+ final TrackGroupArray videoTrackGroupArray = Objects.requireNonNull(
+ trackSelector.getCurrentMappedTrackInfo()).getTrackGroups(videoRenderIndex);
+ if (videoEnabled) {
+ trackSelector.setParameters(trackSelector.buildUponParameters()
+ .clearSelectionOverride(videoRenderIndex, videoTrackGroupArray));
+ } else {
+ trackSelector.setParameters(trackSelector.buildUponParameters()
+ .setSelectionOverride(videoRenderIndex, videoTrackGroupArray, null));
+ }
+ } else if (streamType != StreamType.AUDIO_STREAM
+ && streamType != StreamType.AUDIO_LIVE_STREAM) {
+ reloadPlayQueueManager();
+ }
+
setRecovery();
- reloadPlayQueueManager();
}
//endregion
@@ -4191,7 +4252,7 @@ public final class Player implements
private boolean isLive() {
try {
return !exoPlayerIsNull() && simpleExoPlayer.isCurrentWindowDynamic();
- } catch (@NonNull final IndexOutOfBoundsException e) {
+ } catch (final IndexOutOfBoundsException e) {
// Why would this even happen =(... but lets log it anyway, better safe than sorry
if (DEBUG) {
Log.d(TAG, "player.isCurrentWindowDynamic() failed: ", e);
@@ -4369,15 +4430,31 @@ public final class Player implements
}
private void cleanupVideoSurface() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // >=API23
- if (surfaceHolderCallback != null) {
- if (binding != null) {
- binding.surfaceView.getHolder().removeCallback(surfaceHolderCallback);
- }
- surfaceHolderCallback.release();
- surfaceHolderCallback = null;
+ // Only for API >= 23
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && surfaceHolderCallback != null) {
+ if (binding != null) {
+ binding.surfaceView.getHolder().removeCallback(surfaceHolderCallback);
}
+ surfaceHolderCallback.release();
+ surfaceHolderCallback = null;
}
}
//endregion
+
+ private int getVideoRendererIndex() {
+ final MappingTrackSelector.MappedTrackInfo mappedTrackInfo = trackSelector
+ .getCurrentMappedTrackInfo();
+
+ if (mappedTrackInfo != null) {
+ for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) {
+ final TrackGroupArray trackGroups = mappedTrackInfo.getTrackGroups(i);
+ if (!trackGroups.isEmpty()
+ && simpleExoPlayer.getRendererType(i) == C.TRACK_TYPE_VIDEO) {
+ return i;
+ }
+ }
+ }
+
+ return RENDERER_UNAVAILABLE;
+ }
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java
index 29be402c5..17ecac43e 100644
--- a/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java
+++ b/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java
@@ -44,4 +44,9 @@ public class AudioPlaybackResolver implements PlaybackResolver {
return buildMediaSource(dataSource, audio.getUrl(), PlayerHelper.cacheKeyOf(info, audio),
MediaFormat.getSuffixById(audio.getFormatId()), tag);
}
+
+ @Override
+ public boolean isVideoStreamVideoOnly() {
+ return false;
+ }
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/Resolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/Resolver.java
index a3e1db5b4..8bfe34896 100644
--- a/app/src/main/java/org/schabi/newpipe/player/resolver/Resolver.java
+++ b/app/src/main/java/org/schabi/newpipe/player/resolver/Resolver.java
@@ -6,4 +6,6 @@ import androidx.annotation.Nullable;
public interface Resolver {
@Nullable
Product resolve(@NonNull Source source);
+
+ boolean isVideoStreamVideoOnly();
}
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..f0131c340 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
@@ -35,6 +35,8 @@ public class VideoPlaybackResolver implements PlaybackResolver {
@Nullable
private String playbackQuality;
+ private boolean isVideoStreamVideoOnly = false;
+
public VideoPlaybackResolver(@NonNull final Context context,
@NonNull final PlayerDataSource dataSource,
@NonNull final QualityResolver qualityResolver) {
@@ -46,6 +48,7 @@ public class VideoPlaybackResolver implements PlaybackResolver {
@Override
@Nullable
public MediaSource resolve(@NonNull final StreamInfo info) {
+ isVideoStreamVideoOnly = false;
final MediaSource liveSource = maybeBuildLiveMediaSource(dataSource, info);
if (liveSource != null) {
return liveSource;
@@ -85,6 +88,7 @@ public class VideoPlaybackResolver implements PlaybackResolver {
PlayerHelper.cacheKeyOf(info, audio),
MediaFormat.getSuffixById(audio.getFormatId()), tag);
mediaSources.add(audioSource);
+ isVideoStreamVideoOnly = true;
}
// If there is no audio or video sources, then this media source cannot be played back
@@ -118,6 +122,11 @@ public class VideoPlaybackResolver implements PlaybackResolver {
}
}
+ @Override
+ public boolean isVideoStreamVideoOnly() {
+ return isVideoStreamVideoOnly;
+ }
+
@Nullable
public String getPlaybackQuality() {
return playbackQuality;
From 79f8270c354acbf3ed8430c7f0569983fdfcb71e Mon Sep 17 00:00:00 2001
From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com>
Date: Tue, 2 Nov 2021 19:19:26 +0100
Subject: [PATCH 02/16] Prefer video-only streams to video streams
Prefering video-only streams to video streams for our player will allow us to make seamless transitions on 360 and 720p qualities on YouTube.
External players and the downloader are not affected by this change.
---
.../org/schabi/newpipe/RouterActivity.java | 2 +-
.../newpipe/download/DownloadDialog.java | 2 +-
.../fragments/detail/VideoDetailFragment.java | 1 +
.../resolver/VideoPlaybackResolver.java | 2 +-
.../org/schabi/newpipe/util/ListHelper.java | 97 ++++++++++++-------
.../schabi/newpipe/util/NavigationHelper.java | 18 ++--
.../schabi/newpipe/util/ListHelperTest.java | 11 +--
checkstyle-suppressions.xml | 2 +-
8 files changed, 84 insertions(+), 51 deletions(-)
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 2d9abc6dc..dd3c343aa 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/resolver/VideoPlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java
index f0131c340..751ef822c 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
@@ -58,7 +58,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;
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..26e0f8917 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;
@@ -108,17 +109,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 +133,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,37 +197,63 @@ 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) {
+ @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) {
final ArrayList retList = new ArrayList<>();
final HashMap hashMap = new HashMap<>();
- if (videoOnlyStreams != null) {
- for (final VideoStream stream : videoOnlyStreams) {
- if (!showHigherResolutions
- && HIGH_RESOLUTION_LIST.contains(stream.getResolution())) {
- continue;
+ if (preferVideoOnlyStreams) {
+ if (videoStreams != null) {
+ for (final VideoStream stream : videoStreams) {
+ if (!showHigherResolutions && HIGH_RESOLUTION_LIST.contains(
+ stream.getResolution())) {
+ continue;
+ }
+ retList.add(stream);
}
- retList.add(stream);
}
- }
- if (videoStreams != null) {
- for (final VideoStream stream : videoStreams) {
- if (!showHigherResolutions
- && HIGH_RESOLUTION_LIST.contains(stream.getResolution())) {
- continue;
+ if (videoOnlyStreams != null) {
+ for (final VideoStream stream : videoOnlyStreams) {
+ if (!showHigherResolutions && HIGH_RESOLUTION_LIST.contains(
+ stream.getResolution())) {
+ continue;
+ }
+ retList.add(stream);
+ }
+ }
+ } else {
+ 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);
}
- retList.add(stream);
}
}
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..a5cc20fb4 100644
--- a/app/src/test/java/org/schabi/newpipe/util/ListHelperTest.java
+++ b/app/src/test/java/org/schabi/newpipe/util/ListHelperTest.java
@@ -47,15 +47,10 @@ 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());
for (int i = 0; i < result.size(); i++) {
@@ -67,7 +62,7 @@ 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());
@@ -83,7 +78,7 @@ 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());
diff --git a/checkstyle-suppressions.xml b/checkstyle-suppressions.xml
index f7ed38bdc..0348cf680 100644
--- a/checkstyle-suppressions.xml
+++ b/checkstyle-suppressions.xml
@@ -9,7 +9,7 @@
+ lines="311,343"/>
Date: Tue, 16 Nov 2021 19:25:46 +0100
Subject: [PATCH 03/16] Apply suggestion
---
.../org/schabi/newpipe/player/Player.java | 19 ++++++-------------
1 file changed, 6 insertions(+), 13 deletions(-)
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 bec120baa..771827c20 100644
--- a/app/src/main/java/org/schabi/newpipe/player/Player.java
+++ b/app/src/main/java/org/schabi/newpipe/player/Player.java
@@ -3003,25 +3003,18 @@ public final class Player implements
final MediaSourceTag metadata;
try {
final MediaItem currentMediaItem = simpleExoPlayer.getCurrentMediaItem();
- if (currentMediaItem != null) {
- final MediaItem.PlaybackProperties playbackProperties =
- currentMediaItem.playbackProperties;
- metadata = (MediaSourceTag) (playbackProperties != null ? playbackProperties.tag
- : null);
- } else {
- metadata = null;
+ if (currentMediaItem == null || currentMediaItem.playbackProperties == null
+ || currentMediaItem.playbackProperties.tag == null) {
+ return;
}
- } catch (final IndexOutOfBoundsException | ClassCastException error) {
+ 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) {
From 8ed87e7fbb2a9a24fa8b1d6677049720b9fddbdd Mon Sep 17 00:00:00 2001
From: litetex <40789489+litetex@users.noreply.github.com>
Date: Tue, 16 Nov 2021 20:14:50 +0100
Subject: [PATCH 04/16] Improved ``ListHelper#getSortedStreamVideosList``
---
.../org/schabi/newpipe/util/ListHelper.java | 90 ++++++++-----------
1 file changed, 35 insertions(+), 55 deletions(-)
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 26e0f8917..507bdf6d8 100644
--- a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java
@@ -20,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
@@ -34,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() { }
@@ -213,66 +218,39 @@ public final class ListHelper {
@Nullable final List videoStreams,
@Nullable final List videoOnlyStreams,
final boolean ascendingOrder,
- final boolean preferVideoOnlyStreams) {
- final ArrayList retList = new ArrayList<>();
+ 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 (preferVideoOnlyStreams) {
- if (videoStreams != null) {
- for (final VideoStream stream : videoStreams) {
- if (!showHigherResolutions && HIGH_RESOLUTION_LIST.contains(
- stream.getResolution())) {
- continue;
- }
- retList.add(stream);
- }
- }
- if (videoOnlyStreams != null) {
- for (final VideoStream stream : videoOnlyStreams) {
- if (!showHigherResolutions && HIGH_RESOLUTION_LIST.contains(
- stream.getResolution())) {
- continue;
- }
- retList.add(stream);
- }
- }
- } else {
- 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);
}
/**
@@ -288,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;
}
/**
From a489f40b76b740bc3e548f246f7f29e43ac9cc35 Mon Sep 17 00:00:00 2001
From: litetex <40789489+litetex@users.noreply.github.com>
Date: Tue, 16 Nov 2021 21:00:04 +0100
Subject: [PATCH 05/16] Fixed checkstyle problems
Unable to compile!
* Cleaned up ``getMostCompactAudioIndex`` and ``getHighestQualityAudioIndex`` into a new method ``getAudioIndexByHighestRank``
* Removed unreadable code and use Java Streams API
* Tests work as expected
---
.../org/schabi/newpipe/util/ListHelper.java | 89 ++++++++++---------
1 file changed, 46 insertions(+), 43 deletions(-)
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 507bdf6d8..36cbebf37 100644
--- a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java
@@ -288,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)
+ );
}
/**
@@ -320,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 most compact results 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);
}
/**
From bb27bf9d340ad775e33815c3859cac26ce6caac7 Mon Sep 17 00:00:00 2001
From: litetex <40789489+litetex@users.noreply.github.com>
Date: Tue, 16 Nov 2021 21:40:18 +0100
Subject: [PATCH 06/16] Resolver: Cleaned up ``isVideoStreamVideoOnly``
* Replaced by ``wasLastResolvedVideoAndAudioSeparated``
* Uses an ``Optional`` instead (we can't determine if the video and audio streams are separated when we did not fetch it)
---
.../org/schabi/newpipe/player/Player.java | 6 +-
.../resolver/AudioPlaybackResolver.java | 5 -
.../newpipe/player/resolver/Resolver.java | 2 -
.../resolver/VideoPlaybackResolver.java | 157 ++++++++++--------
4 files changed, 88 insertions(+), 82 deletions(-)
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 771827c20..3cfa74ac2 100644
--- a/app/src/main/java/org/schabi/newpipe/player/Player.java
+++ b/app/src/main/java/org/schabi/newpipe/player/Player.java
@@ -3293,7 +3293,8 @@ public final class Player implements
if (audioPlayerSelected()) {
return audioResolver.resolve(info);
} else {
- if (isAudioOnly && !videoResolver.isVideoStreamVideoOnly()) {
+ if (isAudioOnly
+ && !videoResolver.wasLastResolvedVideoAndAudioSeparated().orElse(false)) {
return audioResolver.resolve(info);
}
@@ -4189,8 +4190,9 @@ public final class Player implements
final boolean isVideoStreamTypeAndIsVideoOnlyStreamOrNoAudioStreamsAvailable =
(streamType == StreamType.VIDEO_STREAM || streamType == StreamType.LIVE_STREAM)
- && (videoResolver.isVideoStreamVideoOnly()
+ && (videoResolver.wasLastResolvedVideoAndAudioSeparated().orElse(false)
|| isNullOrEmpty(info.getAudioStreams()));
+
if (videoRenderIndex != RENDERER_UNAVAILABLE
&& isVideoStreamTypeAndIsVideoOnlyStreamOrNoAudioStreamsAvailable) {
final TrackGroupArray videoTrackGroupArray = Objects.requireNonNull(
diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java
index 17ecac43e..29be402c5 100644
--- a/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java
+++ b/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java
@@ -44,9 +44,4 @@ public class AudioPlaybackResolver implements PlaybackResolver {
return buildMediaSource(dataSource, audio.getUrl(), PlayerHelper.cacheKeyOf(info, audio),
MediaFormat.getSuffixById(audio.getFormatId()), tag);
}
-
- @Override
- public boolean isVideoStreamVideoOnly() {
- return false;
- }
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/Resolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/Resolver.java
index 8bfe34896..a3e1db5b4 100644
--- a/app/src/main/java/org/schabi/newpipe/player/resolver/Resolver.java
+++ b/app/src/main/java/org/schabi/newpipe/player/resolver/Resolver.java
@@ -6,6 +6,4 @@ import androidx.annotation.Nullable;
public interface Resolver {
@Nullable
Product resolve(@NonNull Source source);
-
- boolean isVideoStreamVideoOnly();
}
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 751ef822c..8085e4b3e 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;
@@ -35,7 +36,7 @@ public class VideoPlaybackResolver implements PlaybackResolver {
@Nullable
private String playbackQuality;
- private boolean isVideoStreamVideoOnly = false;
+ private Boolean wasLastResolvedVideoAndAudioSeparated;
public VideoPlaybackResolver(@NonNull final Context context,
@NonNull final PlayerDataSource dataSource,
@@ -48,83 +49,93 @@ public class VideoPlaybackResolver implements PlaybackResolver {
@Override
@Nullable
public MediaSource resolve(@NonNull final StreamInfo info) {
- isVideoStreamVideoOnly = false;
- final MediaSource liveSource = maybeBuildLiveMediaSource(dataSource, info);
- if (liveSource != null) {
- return liveSource;
- }
-
- final List mediaSources = new ArrayList<>();
-
- // Create video stream source
- final List videos = ListHelper.getSortedStreamVideosList(context,
- info.getVideoStreams(), info.getVideoOnlyStreams(), false, true);
- final int index;
- if (videos.isEmpty()) {
- index = -1;
- } else if (playbackQuality == null) {
- index = qualityResolver.getDefaultResolutionIndex(videos);
- } else {
- index = qualityResolver.getOverrideResolutionIndex(videos, getPlaybackQuality());
- }
- final MediaSourceTag tag = new MediaSourceTag(info, videos, index);
- @Nullable final VideoStream video = tag.getSelectedVideoStream();
-
- if (video != null) {
- final MediaSource streamSource = buildMediaSource(dataSource, video.getUrl(),
- PlayerHelper.cacheKeyOf(info, video),
- MediaFormat.getSuffixById(video.getFormatId()), tag);
- mediaSources.add(streamSource);
- }
-
- // Create optional audio stream source
- final List audioStreams = info.getAudioStreams();
- final AudioStream audio = audioStreams.isEmpty() ? null : audioStreams.get(
- ListHelper.getDefaultAudioFormat(context, audioStreams));
- // Use the audio stream if there is no video stream, or
- // Merge with audio stream in case if video does not contain audio
- if (audio != null && (video == null || video.isVideoOnly)) {
- final MediaSource audioSource = buildMediaSource(dataSource, audio.getUrl(),
- PlayerHelper.cacheKeyOf(info, audio),
- MediaFormat.getSuffixById(audio.getFormatId()), tag);
- mediaSources.add(audioSource);
- isVideoStreamVideoOnly = true;
- }
-
- // If there is no audio or video sources, then this media source cannot be played back
- if (mediaSources.isEmpty()) {
- return null;
- }
- // Below are auxiliary media sources
-
- // Create subtitle sources
- if (info.getSubtitles() != null) {
- for (final SubtitlesStream subtitle : info.getSubtitles()) {
- final String mimeType = PlayerHelper.subtitleMimeTypesOf(subtitle.getFormat());
- if (mimeType == null) {
- continue;
- }
- final MediaSource textSource = dataSource.getSampleMediaSourceFactory()
- .createMediaSource(
- new MediaItem.Subtitle(Uri.parse(subtitle.getUrl()),
- mimeType,
- PlayerHelper.captionLanguageOf(context, subtitle)),
- TIME_UNSET);
- mediaSources.add(textSource);
+ boolean isVideoAndAudioSeparated = false;
+ try {
+ final MediaSource liveSource = maybeBuildLiveMediaSource(dataSource, info);
+ if (liveSource != null) {
+ return liveSource;
}
- }
- if (mediaSources.size() == 1) {
- return mediaSources.get(0);
- } else {
- return new MergingMediaSource(mediaSources.toArray(
- new MediaSource[0]));
+ final List mediaSources = new ArrayList<>();
+
+ // Create video stream source
+ final List videos = ListHelper.getSortedStreamVideosList(context,
+ info.getVideoStreams(), info.getVideoOnlyStreams(), false, true);
+ final int index;
+ if (videos.isEmpty()) {
+ index = -1;
+ } else if (playbackQuality == null) {
+ index = qualityResolver.getDefaultResolutionIndex(videos);
+ } else {
+ index = qualityResolver.getOverrideResolutionIndex(videos, getPlaybackQuality());
+ }
+ final MediaSourceTag tag = new MediaSourceTag(info, videos, index);
+ @Nullable final VideoStream video = tag.getSelectedVideoStream();
+
+ if (video != null) {
+ final MediaSource streamSource = buildMediaSource(dataSource, video.getUrl(),
+ PlayerHelper.cacheKeyOf(info, video),
+ MediaFormat.getSuffixById(video.getFormatId()), tag);
+ mediaSources.add(streamSource);
+ }
+
+ // Create optional audio stream source
+ final List audioStreams = info.getAudioStreams();
+ final AudioStream audio = audioStreams.isEmpty() ? null : audioStreams.get(
+ ListHelper.getDefaultAudioFormat(context, audioStreams));
+ // Use the audio stream if there is no video stream, or
+ // Merge with audio stream in case if video does not contain audio
+ if (audio != null && (video == null || video.isVideoOnly)) {
+ final MediaSource audioSource = buildMediaSource(dataSource, audio.getUrl(),
+ PlayerHelper.cacheKeyOf(info, audio),
+ MediaFormat.getSuffixById(audio.getFormatId()), tag);
+ mediaSources.add(audioSource);
+ isVideoAndAudioSeparated = true;
+ }
+
+ // If there is no audio or video sources, then this media source cannot be played back
+ if (mediaSources.isEmpty()) {
+ return null;
+ }
+ // Below are auxiliary media sources
+
+ // Create subtitle sources
+ if (info.getSubtitles() != null) {
+ for (final SubtitlesStream subtitle : info.getSubtitles()) {
+ final String mimeType = PlayerHelper.subtitleMimeTypesOf(subtitle.getFormat());
+ if (mimeType == null) {
+ continue;
+ }
+ final MediaSource textSource = dataSource.getSampleMediaSourceFactory()
+ .createMediaSource(
+ new MediaItem.Subtitle(Uri.parse(subtitle.getUrl()),
+ mimeType,
+ PlayerHelper.captionLanguageOf(context, subtitle)),
+ TIME_UNSET);
+ mediaSources.add(textSource);
+ }
+ }
+
+ if (mediaSources.size() == 1) {
+ return mediaSources.get(0);
+ } else {
+ return new MergingMediaSource(mediaSources.toArray(
+ new MediaSource[0]));
+ }
+ } finally {
+ wasLastResolvedVideoAndAudioSeparated = isVideoAndAudioSeparated;
}
}
- @Override
- public boolean isVideoStreamVideoOnly() {
- return isVideoStreamVideoOnly;
+ /**
+ * Determines if the last resolved StreamInfo had separated audio and video streams
+ * (or only audio).
+ *
+ * @return {@link Optional#empty()} if nothing was resolved
+ * otherwise true
or false
+ */
+ public Optional wasLastResolvedVideoAndAudioSeparated() {
+ return Optional.ofNullable(wasLastResolvedVideoAndAudioSeparated);
}
@Nullable
From 1ea716a31f7cfae490b4cc03ec6d85d2787b79b8 Mon Sep 17 00:00:00 2001
From: litetex <40789489+litetex@users.noreply.github.com>
Date: Tue, 16 Nov 2021 21:46:57 +0100
Subject: [PATCH 07/16] Updated checkstyle suppression
Removed fixed problems.
---
checkstyle-suppressions.xml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/checkstyle-suppressions.xml b/checkstyle-suppressions.xml
index 0348cf680..3c5e4891a 100644
--- a/checkstyle-suppressions.xml
+++ b/checkstyle-suppressions.xml
@@ -8,8 +8,8 @@
lines="232,304"/>
+ files="InfoListAdapter.java"
+ lines="253,325"/>
Date: Mon, 22 Nov 2021 20:02:31 +0100
Subject: [PATCH 08/16] Fixed typo
---
app/src/main/java/org/schabi/newpipe/util/ListHelper.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
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 36cbebf37..c3ccef87c 100644
--- a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java
@@ -320,7 +320,7 @@ public final class ListHelper {
* @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 most compact results or -1 if not found
+ * @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,
From a1c5c9475371128054be5535d9544daa23dcba7e Mon Sep 17 00:00:00 2001
From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com>
Date: Sat, 27 Nov 2021 16:52:54 +0100
Subject: [PATCH 09/16] Add some comments and a JavaDoc
---
.../org/schabi/newpipe/player/Player.java | 26 +++++++++++++++++--
.../resolver/VideoPlaybackResolver.java | 8 +++---
2 files changed, 28 insertions(+), 6 deletions(-)
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 3cfa74ac2..791f8efc7 100644
--- a/app/src/main/java/org/schabi/newpipe/player/Player.java
+++ b/app/src/main/java/org/schabi/newpipe/player/Player.java
@@ -3295,9 +3295,18 @@ public final class Player implements
} else {
if (isAudioOnly
&& !videoResolver.wasLastResolvedVideoAndAudioSeparated().orElse(false)) {
+ // 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).
return videoResolver.resolve(info);
}
}
@@ -4160,8 +4169,8 @@ public final class Player implements
}
isAudioOnly = !videoEnabled;
- // When a user returns from background controls could be hidden
- // but systemUI will be shown 100%. Hide it
+ // When a user returns from background, controls could be hidden but SystemUI will be shown
+ // 100%. Hide it.
if (!isAudioOnly && !isControlsVisible()) {
hideSystemUIIfNeeded();
}
@@ -4198,9 +4207,13 @@ public final class Player implements
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));
}
@@ -4436,6 +4449,15 @@ public final class Player implements
}
//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();
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 8085e4b3e..654f91334 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
@@ -128,11 +128,11 @@ public class VideoPlaybackResolver implements PlaybackResolver {
}
/**
- * Determines if the last resolved StreamInfo had separated audio and video streams
- * (or only audio).
+ * Determine if the last resolved {@link StreamInfo} had separated audio and video streams (or
+ * only audio).
*
- * @return {@link Optional#empty()} if nothing was resolved
- * otherwise true
or false
+ * @return {@link Optional#empty()} if nothing was resolved, otherwise {@code true} or
+ * {@code false}
*/
public Optional wasLastResolvedVideoAndAudioSeparated() {
return Optional.ofNullable(wasLastResolvedVideoAndAudioSeparated);
From 015982bed4b997e06fea3354ed0f65c7ea3aaf65 Mon Sep 17 00:00:00 2001
From: litetex <40789489+litetex@users.noreply.github.com>
Date: Mon, 29 Nov 2021 21:39:42 +0100
Subject: [PATCH 10/16] Extended Tests for ListHelper#getSortedStreamVideosList
* Fixed expected and actual results. They were reversed...
* Added new method ``getSortedStreamVideosListWithPreferVideoOnlyStreamsTest``
---
.../schabi/newpipe/util/ListHelperTest.java | 61 +++++++++++++++++--
1 file changed, 55 insertions(+), 6 deletions(-)
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 a5cc20fb4..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";
@@ -52,9 +54,9 @@ public class ListHelperTest {
List expected = Arrays.asList("144p", "240p", "360p", "480p", "720p", "720p60",
"1080p", "1080p60", "1440p60", "2160p", "2160p60");
- 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);
}
////////////////////
@@ -65,9 +67,56 @@ public class ListHelperTest {
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);
}
}
@@ -81,9 +130,9 @@ public class ListHelperTest {
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);
}
}
From bf02a569eee6a08102364783bbb7fa0282ff6a97 Mon Sep 17 00:00:00 2001
From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com>
Date: Thu, 9 Dec 2021 21:55:56 +0100
Subject: [PATCH 11/16] Fix a NullPointerException when the current metadata is
null
Reload the play queue manager and set the recovery in this case, like on the current behavior (without this PR).
---
.../main/java/org/schabi/newpipe/player/Player.java | 12 +++++++++---
1 file changed, 9 insertions(+), 3 deletions(-)
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 791f8efc7..68601a919 100644
--- a/app/src/main/java/org/schabi/newpipe/player/Player.java
+++ b/app/src/main/java/org/schabi/newpipe/player/Player.java
@@ -4177,9 +4177,15 @@ public final class Player implements
final int videoRenderIndex = getVideoRendererIndex();
- // We can safely assume that currentMetadata is not null (otherwise this method isn't
- // called) so we can use the requireNonNull method of the Objects class.
- final StreamInfo info = Objects.requireNonNull(currentMetadata).getMetadata();
+ // The current metadata may be null sometimes so we will be not able to execute the
+ // block above. Reload the play queue manager in this case.
+ if (currentMetadata == null) {
+ reloadPlayQueueManager();
+ setRecovery();
+ return;
+ }
+
+ final StreamInfo info = currentMetadata.getMetadata();
/* For video streams: we don't want to stream in background the video stream so if the
video stream played is not a video-only stream and if there is an audio stream available,
From 3db37166b4e0e298286e42e94d7249ccfdb5d745 Mon Sep 17 00:00:00 2001
From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com>
Date: Sat, 11 Dec 2021 15:45:25 +0100
Subject: [PATCH 12/16] Apply suggestion
---
app/src/main/java/org/schabi/newpipe/player/Player.java | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
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 68601a919..5b66bea6d 100644
--- a/app/src/main/java/org/schabi/newpipe/player/Player.java
+++ b/app/src/main/java/org/schabi/newpipe/player/Player.java
@@ -4175,16 +4175,17 @@ public final class Player implements
hideSystemUIIfNeeded();
}
- final int videoRenderIndex = getVideoRendererIndex();
-
- // The current metadata may be null sometimes so we will be not able to execute the
- // block above. Reload the play queue manager in this case.
+ // 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 the video stream
+ // played is not video only or when we don't know the index of the video renderer.
if (currentMetadata == null) {
reloadPlayQueueManager();
setRecovery();
return;
}
+ final int videoRenderIndex = getVideoRendererIndex();
final StreamInfo info = currentMetadata.getMetadata();
/* For video streams: we don't want to stream in background the video stream so if the
From ba804c7d4afeb1b4402f6dae3b25cf4e9f864780 Mon Sep 17 00:00:00 2001
From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com>
Date: Thu, 6 Jan 2022 22:00:34 +0100
Subject: [PATCH 13/16] Use a enum to understand better what source type is
used.
This commit also allows a seamless transition for livestreams.
---
.../org/schabi/newpipe/player/Player.java | 28 +++-
.../resolver/VideoPlaybackResolver.java | 141 +++++++++---------
2 files changed, 93 insertions(+), 76 deletions(-)
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 5b66bea6d..a6f4c75f2 100644
--- a/app/src/main/java/org/schabi/newpipe/player/Player.java
+++ b/app/src/main/java/org/schabi/newpipe/player/Player.java
@@ -178,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;
@@ -3293,8 +3294,9 @@ public final class Player implements
if (audioPlayerSelected()) {
return audioResolver.resolve(info);
} else {
- if (isAudioOnly
- && !videoResolver.wasLastResolvedVideoAndAudioSeparated().orElse(false)) {
+ 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.
@@ -4196,18 +4198,30 @@ public final class Player implements
stream will be fetched and the video stream will be fetched again when the user return to a
video player.
- For audio streams: nothing is done, it's not needed to reload the player with the same
- audio stream.
+ For audio streams and audio live streams: nothing is done, it's not needed to reload the
+ player with the same audio stream.
+
+ For video live streams: the play queue manager is not reloaded if the stream source is a
+ live source (see VideoPlaybackResolver#resolve()) and if that's not the case, the
+ requirements for video streams is applied.
In the case where we don't know the index of the video renderer, the play queue manager
is also reloaded. */
final StreamType streamType = info.getStreamType();
+ final SourceType sourceType = videoResolver.getStreamSourceType()
+ .orElse(SourceType.VIDEO_WITH_SEPARATED_AUDIO);
+ final boolean isVideoWithSeparatedAudioOrVideoWithNoSeparatedAudioStreams =
+ sourceType == SourceType.VIDEO_WITH_SEPARATED_AUDIO
+ || (sourceType == SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY
+ && isNullOrEmpty(info.getAudioStreams()));
final boolean isVideoStreamTypeAndIsVideoOnlyStreamOrNoAudioStreamsAvailable =
- (streamType == StreamType.VIDEO_STREAM || streamType == StreamType.LIVE_STREAM)
- && (videoResolver.wasLastResolvedVideoAndAudioSeparated().orElse(false)
- || isNullOrEmpty(info.getAudioStreams()));
+ streamType == StreamType.VIDEO_STREAM
+ && isVideoWithSeparatedAudioOrVideoWithNoSeparatedAudioStreams
+ || (streamType == StreamType.LIVE_STREAM
+ && (sourceType == SourceType.LIVE_STREAM
+ || isVideoWithSeparatedAudioOrVideoWithNoSeparatedAudioStreams));
if (videoRenderIndex != RENDERER_UNAVAILABLE
&& isVideoStreamTypeAndIsVideoOnlyStreamOrNoAudioStreamsAvailable) {
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 654f91334..cbacc2679 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
@@ -32,11 +32,16 @@ public class VideoPlaybackResolver implements PlaybackResolver {
private final PlayerDataSource dataSource;
@NonNull
private final QualityResolver qualityResolver;
+ private SourceType streamSourceType;
@Nullable
private String playbackQuality;
- private Boolean wasLastResolvedVideoAndAudioSeparated;
+ 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,
@@ -49,81 +54,79 @@ public class VideoPlaybackResolver implements PlaybackResolver {
@Override
@Nullable
public MediaSource resolve(@NonNull final StreamInfo info) {
- boolean isVideoAndAudioSeparated = false;
- try {
- final MediaSource liveSource = maybeBuildLiveMediaSource(dataSource, info);
- if (liveSource != null) {
- return liveSource;
- }
+ final MediaSource liveSource = maybeBuildLiveMediaSource(dataSource, info);
+ if (liveSource != null) {
+ streamSourceType = SourceType.LIVE_STREAM;
+ return liveSource;
+ }
- final List mediaSources = new ArrayList<>();
+ final List mediaSources = new ArrayList<>();
- // Create video stream source
- final List videos = ListHelper.getSortedStreamVideosList(context,
- info.getVideoStreams(), info.getVideoOnlyStreams(), false, true);
- final int index;
- if (videos.isEmpty()) {
- index = -1;
- } else if (playbackQuality == null) {
- index = qualityResolver.getDefaultResolutionIndex(videos);
- } else {
- index = qualityResolver.getOverrideResolutionIndex(videos, getPlaybackQuality());
- }
- final MediaSourceTag tag = new MediaSourceTag(info, videos, index);
- @Nullable final VideoStream video = tag.getSelectedVideoStream();
+ // Create video stream source
+ final List videos = ListHelper.getSortedStreamVideosList(context,
+ info.getVideoStreams(), info.getVideoOnlyStreams(), false, true);
+ final int index;
+ if (videos.isEmpty()) {
+ index = -1;
+ } else if (playbackQuality == null) {
+ index = qualityResolver.getDefaultResolutionIndex(videos);
+ } else {
+ index = qualityResolver.getOverrideResolutionIndex(videos, getPlaybackQuality());
+ }
+ final MediaSourceTag tag = new MediaSourceTag(info, videos, index);
+ @Nullable final VideoStream video = tag.getSelectedVideoStream();
- if (video != null) {
- final MediaSource streamSource = buildMediaSource(dataSource, video.getUrl(),
- PlayerHelper.cacheKeyOf(info, video),
- MediaFormat.getSuffixById(video.getFormatId()), tag);
- mediaSources.add(streamSource);
- }
+ if (video != null) {
+ final MediaSource streamSource = buildMediaSource(dataSource, video.getUrl(),
+ PlayerHelper.cacheKeyOf(info, video),
+ MediaFormat.getSuffixById(video.getFormatId()), tag);
+ mediaSources.add(streamSource);
+ }
- // Create optional audio stream source
- final List audioStreams = info.getAudioStreams();
- final AudioStream audio = audioStreams.isEmpty() ? null : audioStreams.get(
- ListHelper.getDefaultAudioFormat(context, audioStreams));
- // Use the audio stream if there is no video stream, or
- // Merge with audio stream in case if video does not contain audio
- if (audio != null && (video == null || video.isVideoOnly)) {
- final MediaSource audioSource = buildMediaSource(dataSource, audio.getUrl(),
- PlayerHelper.cacheKeyOf(info, audio),
- MediaFormat.getSuffixById(audio.getFormatId()), tag);
- mediaSources.add(audioSource);
- isVideoAndAudioSeparated = true;
- }
+ // Create optional audio stream source
+ final List audioStreams = info.getAudioStreams();
+ final AudioStream audio = audioStreams.isEmpty() ? null : audioStreams.get(
+ ListHelper.getDefaultAudioFormat(context, audioStreams));
+ // Use the audio stream if there is no video stream, or
+ // Merge with audio stream in case if video does not contain audio
+ if (audio != null && (video == null || video.isVideoOnly)) {
+ final MediaSource audioSource = buildMediaSource(dataSource, audio.getUrl(),
+ 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
- if (mediaSources.isEmpty()) {
- return null;
- }
- // Below are auxiliary media sources
+ // If there is no audio or video sources, then this media source cannot be played back
+ if (mediaSources.isEmpty()) {
+ return null;
+ }
+ // Below are auxiliary media sources
- // Create subtitle sources
- if (info.getSubtitles() != null) {
- for (final SubtitlesStream subtitle : info.getSubtitles()) {
- final String mimeType = PlayerHelper.subtitleMimeTypesOf(subtitle.getFormat());
- if (mimeType == null) {
- continue;
- }
- final MediaSource textSource = dataSource.getSampleMediaSourceFactory()
- .createMediaSource(
- new MediaItem.Subtitle(Uri.parse(subtitle.getUrl()),
- mimeType,
- PlayerHelper.captionLanguageOf(context, subtitle)),
- TIME_UNSET);
- mediaSources.add(textSource);
+ // Create subtitle sources
+ if (info.getSubtitles() != null) {
+ for (final SubtitlesStream subtitle : info.getSubtitles()) {
+ final String mimeType = PlayerHelper.subtitleMimeTypesOf(subtitle.getFormat());
+ if (mimeType == null) {
+ continue;
}
+ final MediaSource textSource = dataSource.getSampleMediaSourceFactory()
+ .createMediaSource(
+ new MediaItem.Subtitle(Uri.parse(subtitle.getUrl()),
+ mimeType,
+ PlayerHelper.captionLanguageOf(context, subtitle)),
+ TIME_UNSET);
+ mediaSources.add(textSource);
}
+ }
- if (mediaSources.size() == 1) {
- return mediaSources.get(0);
- } else {
- return new MergingMediaSource(mediaSources.toArray(
- new MediaSource[0]));
- }
- } finally {
- wasLastResolvedVideoAndAudioSeparated = isVideoAndAudioSeparated;
+ if (mediaSources.size() == 1) {
+ return mediaSources.get(0);
+ } else {
+ return new MergingMediaSource(mediaSources.toArray(
+ new MediaSource[0]));
}
}
@@ -134,8 +137,8 @@ public class VideoPlaybackResolver implements PlaybackResolver {
* @return {@link Optional#empty()} if nothing was resolved, otherwise {@code true} or
* {@code false}
*/
- public Optional wasLastResolvedVideoAndAudioSeparated() {
- return Optional.ofNullable(wasLastResolvedVideoAndAudioSeparated);
+ public Optional getStreamSourceType() {
+ return Optional.ofNullable(streamSourceType);
}
@Nullable
From d27d36b76a9a8310534757277144ffda8cb74f75 Mon Sep 17 00:00:00 2001
From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com>
Date: Sun, 30 Jan 2022 19:45:38 +0100
Subject: [PATCH 14/16] Adress requested changes
---
.../org/schabi/newpipe/player/Player.java | 137 ++++++++++++------
1 file changed, 90 insertions(+), 47 deletions(-)
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 a6f4c75f2..5b2820b4e 100644
--- a/app/src/main/java/org/schabi/newpipe/player/Player.java
+++ b/app/src/main/java/org/schabi/newpipe/player/Player.java
@@ -197,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;
@@ -4179,8 +4180,8 @@ public final class Player implements
// 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 the video stream
- // played is not video only or when we don't know the index of the video renderer.
+ // 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();
@@ -4190,41 +4191,22 @@ public final class Player implements
final int videoRenderIndex = getVideoRendererIndex();
final StreamInfo info = currentMetadata.getMetadata();
- /* For video streams: we don't want to stream in background the video stream so if the
- video stream played is not a video-only stream and if there is an audio stream available,
- play this audio stream in background by reloading the play queue manager.
- Otherwise the video renderer will be just disabled (because there is no
- other stream for it to play the audio): if the video stream is video-only, only the audio
- stream will be fetched and the video stream will be fetched again when the user return to a
- video player.
+ // 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);
- For audio streams and audio live streams: nothing is done, it's not needed to reload the
- player with the same audio stream.
+ 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;
+ }
- For video live streams: the play queue manager is not reloaded if the stream source is a
- live source (see VideoPlaybackResolver#resolve()) and if that's not the case, the
- requirements for video streams is applied.
-
- In the case where we don't know the index of the video renderer, the play queue manager
- is also reloaded. */
-
- final StreamType streamType = info.getStreamType();
- final SourceType sourceType = videoResolver.getStreamSourceType()
- .orElse(SourceType.VIDEO_WITH_SEPARATED_AUDIO);
-
- final boolean isVideoWithSeparatedAudioOrVideoWithNoSeparatedAudioStreams =
- sourceType == SourceType.VIDEO_WITH_SEPARATED_AUDIO
- || (sourceType == SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY
- && isNullOrEmpty(info.getAudioStreams()));
- final boolean isVideoStreamTypeAndIsVideoOnlyStreamOrNoAudioStreamsAvailable =
- streamType == StreamType.VIDEO_STREAM
- && isVideoWithSeparatedAudioOrVideoWithNoSeparatedAudioStreams
- || (streamType == StreamType.LIVE_STREAM
- && (sourceType == SourceType.LIVE_STREAM
- || isVideoWithSeparatedAudioOrVideoWithNoSeparatedAudioStreams));
-
- if (videoRenderIndex != RENDERER_UNAVAILABLE
- && isVideoStreamTypeAndIsVideoOnlyStreamOrNoAudioStreamsAvailable) {
final TrackGroupArray videoTrackGroupArray = Objects.requireNonNull(
trackSelector.getCurrentMappedTrackInfo()).getTrackGroups(videoRenderIndex);
if (videoEnabled) {
@@ -4238,13 +4220,72 @@ public final class Player implements
trackSelector.setParameters(trackSelector.buildUponParameters()
.setSelectionOverride(videoRenderIndex, videoTrackGroupArray, null));
}
- } else if (streamType != StreamType.AUDIO_STREAM
- && streamType != StreamType.AUDIO_LIVE_STREAM) {
- reloadPlayQueueManager();
}
setRecovery();
}
+
+ /**
+ * 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
@@ -4483,16 +4524,18 @@ public final class Player implements
final MappingTrackSelector.MappedTrackInfo mappedTrackInfo = trackSelector
.getCurrentMappedTrackInfo();
- if (mappedTrackInfo != null) {
- for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) {
- final TrackGroupArray trackGroups = mappedTrackInfo.getTrackGroups(i);
- if (!trackGroups.isEmpty()
- && simpleExoPlayer.getRendererType(i) == C.TRACK_TYPE_VIDEO) {
- return i;
- }
- }
+ if (mappedTrackInfo == null) {
+ return RENDERER_UNAVAILABLE;
}
- 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);
}
}
From 8932adbf88b47fa6334327e18654e371dd472b94 Mon Sep 17 00:00:00 2001
From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com>
Date: Sun, 13 Feb 2022 16:54:44 +0100
Subject: [PATCH 15/16] Apply suggested change and add a note about data
consumption for HLS streams in background
ExoPlayer right now fetches HLS video tracks even if you disable them (with setRendererDisabled or setSelectionOverride).
See issue 9282 of ExoPlayer's issue tracker for more information.
---
.../org/schabi/newpipe/player/Player.java | 35 ++++++++++---------
1 file changed, 18 insertions(+), 17 deletions(-)
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 5b2820b4e..8cc0dbd23 100644
--- a/app/src/main/java/org/schabi/newpipe/player/Player.java
+++ b/app/src/main/java/org/schabi/newpipe/player/Player.java
@@ -3294,24 +3294,25 @@ public final class Player implements
public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) {
if (audioPlayerSelected()) {
return audioResolver.resolve(info);
- } else {
- 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).
- return videoResolver.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() {
From c5fc37150d38d89a430ad92cb33665dfcf7b7b35 Mon Sep 17 00:00:00 2001
From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com>
Date: Sun, 20 Feb 2022 19:35:58 +0100
Subject: [PATCH 16/16] Update JavaDoc of
VideoPlaybackResolver.getStreamSourceType()
---
.../newpipe/player/resolver/VideoPlaybackResolver.java | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
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 cbacc2679..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
@@ -131,11 +131,10 @@ public class VideoPlaybackResolver implements PlaybackResolver {
}
/**
- * Determine if the last resolved {@link StreamInfo} had separated audio and video streams (or
- * only audio).
+ * Returns the last resolved {@link StreamInfo}'s {@link SourceType source type}.
*
- * @return {@link Optional#empty()} if nothing was resolved, otherwise {@code true} or
- * {@code false}
+ * @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);