1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2025-11-15 06:27:16 +00:00

Added the ability to limit video quality if using mobile data.

* Added a dropdown to video & audio settings
* Changes to ListHelper:
** Limits resolution when code requests the default video resolution
** Limits audio bitrate when code requests the default audio bitrate
** Removed some dead code and did some cleanup
** Make methods private/protected to help understand what was in use
** The code now chooses one format over an other using a simple raking system defined in array constants. I realized I needed to do this in order to choose the most efficient video stream. I did my best to evaluate the video and audio formats based on quality and efficiency. It's not an exact science.
** Made changes to the tests to support my changes
This commit is contained in:
James Straub
2018-04-21 12:35:04 -04:00
parent 7d427b4cc4
commit d1b0cd74be
7 changed files with 432 additions and 119 deletions

View File

@@ -645,7 +645,7 @@ public final class MainVideoPlayer extends AppCompatActivity
@Override
protected int getOverrideResolutionIndex(final List<VideoStream> sortedVideos,
final String playbackQuality) {
return ListHelper.getDefaultResolutionIndex(context, sortedVideos, playbackQuality);
return ListHelper.getResolutionIndex(context, sortedVideos, playbackQuality);
}
/*//////////////////////////////////////////////////////////////////////////

View File

@@ -509,7 +509,7 @@ public final class PopupVideoPlayer extends Service {
@Override
protected int getOverrideResolutionIndex(final List<VideoStream> sortedVideos,
final String playbackQuality) {
return ListHelper.getPopupDefaultResolutionIndex(context, sortedVideos, playbackQuality);
return ListHelper.getPopupResolutionIndex(context, sortedVideos, playbackQuality);
}
/*//////////////////////////////////////////////////////////////////////////

View File

@@ -2,6 +2,7 @@ package org.schabi.newpipe.util;
import android.content.Context;
import android.content.SharedPreferences;
import android.net.ConnectivityManager;
import android.preference.PreferenceManager;
import android.support.annotation.StringRes;
@@ -13,56 +14,38 @@ import org.schabi.newpipe.extractor.stream.VideoStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
@SuppressWarnings("WeakerAccess")
public final class ListHelper {
// Video format in order of quality. 0=lowest quality, n=highest quality
private static final List<MediaFormat> VIDEO_FORMAT_QUALITY_RANKING =
Arrays.asList(MediaFormat.v3GPP, MediaFormat.WEBM, MediaFormat.MPEG_4);
// Audio format in order of quality. 0=lowest quality, n=highest quality
private static final List<MediaFormat> AUDIO_FORMAT_QUALITY_RANKING =
Arrays.asList(MediaFormat.MP3, MediaFormat.WEBMA, MediaFormat.M4A);
// Audio format in order of efficiency. 0=most efficient, n=least efficient
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");
/**
* Return the index of the default stream in the list, based on the parameters
* defaultResolution and defaultFormat
*
* @return index of the default resolution&format
*/
public static int getDefaultResolutionIndex(String defaultResolution, String bestResolutionKey, MediaFormat defaultFormat, List<VideoStream> videoStreams) {
if (videoStreams == null || videoStreams.isEmpty()) return -1;
sortStreamList(videoStreams, false);
if (defaultResolution.equals(bestResolutionKey)) {
return 0;
}
int defaultStreamIndex = getDefaultStreamIndex(defaultResolution, defaultFormat, videoStreams);
if (defaultStreamIndex == -1 && defaultResolution.contains("p60")) {
defaultStreamIndex = getDefaultStreamIndex(defaultResolution.replace("p60", "p"), defaultFormat, videoStreams);
}
// this is actually an error,
// but maybe there is really no stream fitting to the default value.
if (defaultStreamIndex == -1) return 0;
return defaultStreamIndex;
}
/**
* @see #getDefaultResolutionIndex(String, String, MediaFormat, List)
*/
public static int getDefaultResolutionIndex(Context context, List<VideoStream> videoStreams) {
SharedPreferences defaultPreferences = PreferenceManager.getDefaultSharedPreferences(context);
if (defaultPreferences == null) return 0;
String defaultResolution = defaultPreferences.getString(context.getString(R.string.default_resolution_key), context.getString(R.string.default_resolution_value));
return getDefaultResolutionIndex(context, videoStreams, defaultResolution);
String defaultResolution = computeDefaultResolution(context,
R.string.default_resolution_key, R.string.default_resolution_value);
return getDefaultResolutionWithDefaultFormat(context, defaultResolution, videoStreams);
}
/**
* @see #getDefaultResolutionIndex(String, String, MediaFormat, List)
*/
public static int getDefaultResolutionIndex(Context context, List<VideoStream> videoStreams, String defaultResolution) {
public static int getResolutionIndex(Context context, List<VideoStream> videoStreams, String defaultResolution) {
return getDefaultResolutionWithDefaultFormat(context, defaultResolution, videoStreams);
}
@@ -70,69 +53,33 @@ public final class ListHelper {
* @see #getDefaultResolutionIndex(String, String, MediaFormat, List)
*/
public static int getPopupDefaultResolutionIndex(Context context, List<VideoStream> videoStreams) {
SharedPreferences defaultPreferences = PreferenceManager.getDefaultSharedPreferences(context);
if (defaultPreferences == null) return 0;
String defaultResolution = defaultPreferences.getString(context.getString(R.string.default_popup_resolution_key), context.getString(R.string.default_popup_resolution_value));
return getPopupDefaultResolutionIndex(context, videoStreams, defaultResolution);
String defaultResolution = computeDefaultResolution(context,
R.string.default_popup_resolution_key, R.string.default_popup_resolution_value);
return getDefaultResolutionWithDefaultFormat(context, defaultResolution, videoStreams);
}
/**
* @see #getDefaultResolutionIndex(String, String, MediaFormat, List)
*/
public static int getPopupDefaultResolutionIndex(Context context, List<VideoStream> videoStreams, String defaultResolution) {
public static int getPopupResolutionIndex(Context context, List<VideoStream> videoStreams, String defaultResolution) {
return getDefaultResolutionWithDefaultFormat(context, defaultResolution, videoStreams);
}
public static int getDefaultAudioFormat(Context context, List<AudioStream> audioStreams) {
MediaFormat defaultFormat = getDefaultFormat(context, R.string.default_audio_format_key, R.string.default_audio_format_value);
return getHighestQualityAudioIndex(defaultFormat, audioStreams);
}
MediaFormat defaultFormat = getDefaultFormat(context, R.string.default_audio_format_key,
R.string.default_audio_format_value);
public static int getHighestQualityAudioIndex(List<AudioStream> audioStreams) {
if (audioStreams == null || audioStreams.isEmpty()) return -1;
int highestQualityIndex = 0;
if (audioStreams.size() > 1) for (int i = 1; i < audioStreams.size(); i++) {
AudioStream audioStream = audioStreams.get(i);
if (audioStream.getAverageBitrate() >= audioStreams.get(highestQualityIndex).getAverageBitrate()) highestQualityIndex = i;
// If the user has chosen to limit resolution to conserve mobile data
// usage then we should also limit our audio usage.
int result;
if (isLimitingDataUsage(context)) {
result = getMostCompactAudioIndex(defaultFormat, audioStreams);
}
return highestQualityIndex;
}
/**
* Get the audio from the list with the highest bitrate
*
* @param audioStreams list the audio streams
* @return audio with highest average bitrate
*/
public static AudioStream getHighestQualityAudio(List<AudioStream> audioStreams) {
if (audioStreams == null || audioStreams.isEmpty()) return null;
return audioStreams.get(getHighestQualityAudioIndex(audioStreams));
}
/**
* Get the audio from the list with the highest bitrate
*
* @param audioStreams list the audio streams
* @return index of the audio with the highest average bitrate of the default format
*/
public static int getHighestQualityAudioIndex(MediaFormat defaultFormat, List<AudioStream> audioStreams) {
if (audioStreams == null || audioStreams.isEmpty() || defaultFormat == null) return -1;
int highestQualityIndex = -1;
for (int i = 0; i < audioStreams.size(); i++) {
AudioStream audioStream = audioStreams.get(i);
if (highestQualityIndex == -1 && audioStream.getFormat() == defaultFormat) highestQualityIndex = i;
if (highestQualityIndex != -1 && audioStream.getFormat() == defaultFormat
&& audioStream.getAverageBitrate() > audioStreams.get(highestQualityIndex).getAverageBitrate()) {
highestQualityIndex = i;
}
else {
result = getHighestQualityAudioIndex(defaultFormat, audioStreams);
}
if (highestQualityIndex == -1) highestQualityIndex = getHighestQualityAudioIndex(audioStreams);
return highestQualityIndex;
return result;
}
/**
@@ -154,6 +101,49 @@ public final class ListHelper {
return getSortedStreamVideosList(defaultFormat, showHigherResolutions, videoStreams, videoOnlyStreams, ascendingOrder);
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
private static String computeDefaultResolution(Context context, int key, int value) {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
// Load the prefered resolution otherwise the best available
String resolution = preferences != null ? preferences.getString(context.getString(key),
context.getString(value)) : context.getString(R.string.best_resolution_key);
String maxResolution = getResolutionLimit(context);
if (maxResolution != null && compareVideoStreamResolution(maxResolution, resolution) < 1){
resolution = maxResolution;
}
return resolution;
}
/**
* Return the index of the default stream in the list, based on the parameters
* defaultResolution and defaultFormat
*
* @return index of the default resolution&format
*/
static int getDefaultResolutionIndex(String defaultResolution, String bestResolutionKey,
MediaFormat defaultFormat, List<VideoStream> videoStreams) {
if (videoStreams == null || videoStreams.isEmpty()) return -1;
sortStreamList(videoStreams, false);
if (defaultResolution.equals(bestResolutionKey)) {
return 0;
}
int defaultStreamIndex = getVideoStreamIndex(defaultResolution, defaultFormat, videoStreams);
// this is actually an error,
// but maybe there is really no stream fitting to the default value.
if (defaultStreamIndex == -1) {
return 0;
}
return defaultStreamIndex;
}
/**
* Join the two lists of video streams (video_only and normal videos), and sort them according with default format
* chosen by the user
@@ -165,7 +155,7 @@ public final class ListHelper {
* @param ascendingOrder true -> smallest to greatest | false -> greatest to smallest @return the sorted list
* @return the sorted list
*/
public static List<VideoStream> getSortedStreamVideosList(MediaFormat defaultFormat, boolean showHigherResolutions, List<VideoStream> videoStreams, List<VideoStream> videoOnlyStreams, boolean ascendingOrder) {
static List<VideoStream> getSortedStreamVideosList(MediaFormat defaultFormat, boolean showHigherResolutions, List<VideoStream> videoStreams, List<VideoStream> videoOnlyStreams, boolean ascendingOrder) {
ArrayList<VideoStream> retList = new ArrayList<>();
HashMap<String, VideoStream> hashMap = new HashMap<>();
@@ -215,36 +205,138 @@ public final class ListHelper {
* @param videoStreams list that the sorting will be applied
* @param ascendingOrder true -> smallest to greatest | false -> greatest to smallest
*/
public static void sortStreamList(List<VideoStream> videoStreams, final boolean ascendingOrder) {
Collections.sort(videoStreams, new Comparator<VideoStream>() {
@Override
public int compare(VideoStream o1, VideoStream o2) {
int res1 = Integer.parseInt(o1.getResolution().replace("0p60", "1").replaceAll("[^\\d.]", ""));
int res2 = Integer.parseInt(o2.getResolution().replace("0p60", "1").replaceAll("[^\\d.]", ""));
return ascendingOrder ? res1 - res2 : res2 - res1;
}
private static void sortStreamList(List<VideoStream> videoStreams, final boolean ascendingOrder) {
Collections.sort(videoStreams, (o1, o2) -> {
int result = compareVideoStreamResolution(o1, o2, VIDEO_FORMAT_QUALITY_RANKING);
return result == 0 ? 0 : (ascendingOrder ? result : -result);
});
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
/**
* Get the audio from the list with the highest quality. Format will be ignored if it yields
* no results.
*
* @param audioStreams list the audio streams
* @return index of the audio with the highest average bitrate of the default format
*/
static int getHighestQualityAudioIndex(MediaFormat format, List<AudioStream> audioStreams) {
int result = -1;
if (audioStreams != null) {
while(result == -1) {
AudioStream prevStream = null;
for (int idx = 0; idx < audioStreams.size(); idx++) {
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;
}
private static int getDefaultStreamIndex(String defaultResolution, MediaFormat defaultFormat, List<VideoStream> videoStreams) {
int defaultStreamIndex = -1;
for (int i = 0; i < videoStreams.size(); i++) {
VideoStream stream = videoStreams.get(i);
if (defaultStreamIndex == -1 && stream.getResolution().equals(defaultResolution)) defaultStreamIndex = i;
/**
* Get the audio from the list with the lowest bitrate and efficient format. Format will be
* ignored if it yields no results.
*
* @param format The target format type or null if it doesn't matter
* @param audioStreams list the audio streams
* @return index of the audio stream that can produce the most compact results or -1 if not found.
*/
static int getMostCompactAudioIndex(MediaFormat format, List<AudioStream> audioStreams) {
int result = -1;
if (audioStreams != null) {
while(result == -1) {
AudioStream prevStream = null;
for (int idx = 0; idx < audioStreams.size(); idx++) {
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;
}
}
return result;
}
if (stream.getFormat() == defaultFormat && stream.getResolution().equals(defaultResolution)) {
return i;
/**
* Locates a possible match for the given resolution and format in the provided list.
* In this order:
* 1. Find a format and resolution match
* 2. Find a format and resolution match and ignore the refresh
* 3. Find a resolution match
* 4. Find a resolution match and ignore the refresh
* 5. Find a resolution just below the requested resolution and ignore the refresh
* 6. Give up
*/
static int getVideoStreamIndex(String targetResolution, MediaFormat targetFormat,
List<VideoStream> videoStreams) {
int fullMatchIndex = -1;
int fullMatchNoRefreshIndex = -1;
int resMatchOnlyIndex = -1;
int resMatchOnlyNoRefreshIndex = -1;
int lowerResMatchNoRefreshIndex = -1;
String targetResolutionNoRefresh = targetResolution.replaceAll("p\\d+$", "p");
for (int idx = 0; idx < videoStreams.size(); idx++) {
MediaFormat format = targetFormat == null ? null : videoStreams.get(idx).getFormat();
String resolution = videoStreams.get(idx).getResolution();
String resolutionNoRefresh = resolution.replaceAll("p\\d+$", "p");
if (format == targetFormat && resolution.equals(targetResolution)) {
fullMatchIndex = idx;
}
if (format == targetFormat && resolutionNoRefresh.equals(targetResolutionNoRefresh)) {
fullMatchNoRefreshIndex = idx;
}
if (resMatchOnlyIndex == -1 && resolution.equals(targetResolution)) {
resMatchOnlyIndex = idx;
}
if (resMatchOnlyNoRefreshIndex == -1 && resolutionNoRefresh.equals(targetResolutionNoRefresh)) {
resMatchOnlyNoRefreshIndex = idx;
}
if (lowerResMatchNoRefreshIndex == -1 && compareVideoStreamResolution(resolutionNoRefresh, targetResolutionNoRefresh) < 0) {
lowerResMatchNoRefreshIndex = idx;
}
}
return defaultStreamIndex;
if (fullMatchIndex != -1) {
return fullMatchIndex;
}
if (fullMatchNoRefreshIndex != -1) {
return fullMatchNoRefreshIndex;
}
if (resMatchOnlyIndex != -1) {
return resMatchOnlyIndex;
}
if (resMatchOnlyNoRefreshIndex != -1) {
return resMatchOnlyNoRefreshIndex;
}
return lowerResMatchNoRefreshIndex;
}
/**
* Fetches the desired resolution or returns the default if it is not found. The resolution
* will be reduced if video chocking is active.
*/
private static int getDefaultResolutionWithDefaultFormat(Context context, String defaultResolution, List<VideoStream> videoStreams) {
MediaFormat defaultFormat = getDefaultFormat(context, R.string.default_video_format_key, R.string.default_video_format_value);
return getDefaultResolutionIndex(defaultResolution, context.getString(R.string.best_resolution_key), defaultFormat, videoStreams);
@@ -280,4 +372,85 @@ public final class ListHelper {
}
return format;
}
// Compares the quality of two audio streams
private static int compareAudioStreamBitrate(AudioStream streamA, AudioStream streamB,
List<MediaFormat> formatRanking) {
if (streamA == null) {
return -1;
}
if (streamB == null) {
return 1;
}
if (streamA.getAverageBitrate() < streamB.getAverageBitrate()) {
return -1;
}
if (streamA.getAverageBitrate() > streamB.getAverageBitrate()) {
return 1;
}
// Same bitrate and format
return formatRanking.indexOf(streamA.getFormat()) - formatRanking.indexOf(streamB.getFormat());
}
private static int compareVideoStreamResolution(String r1, String r2) {
int res1 = Integer.parseInt(r1.replaceAll("0p\\d+$", "1")
.replaceAll("[^\\d.]", ""));
int res2 = Integer.parseInt(r2.replaceAll("0p\\d+$", "1")
.replaceAll("[^\\d.]", ""));
return res1 - res2;
}
// Compares the quality of two video streams.
private static int compareVideoStreamResolution(VideoStream streamA, VideoStream streamB,
List<MediaFormat> formatRanking) {
if (streamA == null) {
return -1;
}
if (streamB == null) {
return 1;
}
int resComp = compareVideoStreamResolution(streamA.getResolution(), streamB.getResolution());
if (resComp != 0) {
return resComp;
}
// Same bitrate and format
return formatRanking.indexOf(streamA.getFormat()) - formatRanking.indexOf(streamB.getFormat());
}
private static boolean isLimitingDataUsage(Context context) {
return getResolutionLimit(context) != null;
}
/**
* The maximum resolution allowed
* @param context App context
* @return maximum resolution allowed or null if there is no maximum
*/
private static String getResolutionLimit(Context context) {
String resolutionLimit = null;
if (!isWifiActive(context)) {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
String defValue = context.getString(R.string.limit_data_usage_none_key);
String value = preferences.getString(
context.getString(R.string.limit_mobile_data_usage_key), defValue);
resolutionLimit = value.equals(defValue) ? null : value;
}
return resolutionLimit;
}
/**
* Are we connected to wifi?
* @param context App context
* @return True if connected to wifi
*/
private static boolean isWifiActive(Context context)
{
ConnectivityManager manager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
return manager.getActiveNetworkInfo().getType() == ConnectivityManager.TYPE_WIFI;
}
}