diff --git a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java index 502e5b802..fbf0000bb 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -647,7 +647,7 @@ public final class MainVideoPlayer extends AppCompatActivity @Override protected int getOverrideResolutionIndex(final List sortedVideos, final String playbackQuality) { - return ListHelper.getDefaultResolutionIndex(context, sortedVideos, playbackQuality); + return ListHelper.getResolutionIndex(context, sortedVideos, playbackQuality); } /*////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java index 1dcf4a89d..8107345a1 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -517,7 +517,7 @@ public final class PopupVideoPlayer extends Service { @Override protected int getOverrideResolutionIndex(final List sortedVideos, final String playbackQuality) { - return ListHelper.getPopupDefaultResolutionIndex(context, sortedVideos, playbackQuality); + return ListHelper.getPopupResolutionIndex(context, sortedVideos, playbackQuality); } /*////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java index 79fd1e496..4f607b581 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java @@ -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 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 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 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"); - /** - * 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 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 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 videoStreams, String defaultResolution) { + public static int getResolutionIndex(Context context, List videoStreams, String defaultResolution) { return getDefaultResolutionWithDefaultFormat(context, defaultResolution, videoStreams); } @@ -70,69 +53,29 @@ public final class ListHelper { * @see #getDefaultResolutionIndex(String, String, MediaFormat, List) */ public static int getPopupDefaultResolutionIndex(Context context, List 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 videoStreams, String defaultResolution) { + public static int getPopupResolutionIndex(Context context, List videoStreams, String defaultResolution) { return getDefaultResolutionWithDefaultFormat(context, defaultResolution, videoStreams); } public static int getDefaultAudioFormat(Context context, List 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 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. + if (isLimitingDataUsage(context)) { + return getMostCompactAudioIndex(defaultFormat, audioStreams); + } else { + return getHighestQualityAudioIndex(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 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 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; - } - } - if (highestQualityIndex == -1) highestQualityIndex = getHighestQualityAudioIndex(audioStreams); - return highestQualityIndex; } /** @@ -154,6 +97,50 @@ 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 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 +152,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 getSortedStreamVideosList(MediaFormat defaultFormat, boolean showHigherResolutions, List videoStreams, List videoOnlyStreams, boolean ascendingOrder) { + static List getSortedStreamVideosList(MediaFormat defaultFormat, boolean showHigherResolutions, List videoStreams, List videoOnlyStreams, boolean ascendingOrder) { ArrayList retList = new ArrayList<>(); HashMap hashMap = new HashMap<>(); @@ -215,36 +202,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 videoStreams, final boolean ascendingOrder) { - Collections.sort(videoStreams, new Comparator() { - @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 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 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 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 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 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 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 +369,85 @@ public final class ListHelper { } return format; } + + // Compares the quality of two audio streams + private static int compareAudioStreamBitrate(AudioStream streamA, AudioStream streamB, + List 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 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; + } } diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index 20a6ab68f..438cbc0c2 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -853,4 +853,22 @@ ZM ZW + + + limit_mobile_data_usage + limit_data_usage_none + + + @string/limit_data_usage_none_key + 1080p60 + 1080p + 720p60 + 720p + 480p + 360p + 240p + 144p + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bdfb7e0f4..5ee80536f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -483,8 +483,25 @@ Nightcore Default - + In order to comply with the European General Data Protection Regulation (GDPR), we herby draw your attention to NewPipe\'s privacy policy. Please read it carefully.\nYou must accept it to send us the bug report. Accept Decline + + + No limit + Limit resolution when using mobile data + @string/limit_data_usage_none_key + + @string/limit_data_usage_none_description + 1080p60 + 1080p + 720p60 + 720p + 480p + 360p + 240p + 144p + + diff --git a/app/src/main/res/xml/video_audio_settings.xml b/app/src/main/res/xml/video_audio_settings.xml index 6a3b7a0b3..6ec0da215 100644 --- a/app/src/main/res/xml/video_audio_settings.xml +++ b/app/src/main/res/xml/video_audio_settings.xml @@ -19,6 +19,14 @@ android:summary="%s" android:title="@string/default_popup_resolution_title"/> + + + android:title="@string/default_audio_format_title" /> ())); } + @Test + public void getLowestQualityAudioFormatTest() throws Exception { + AudioStream stream = audioStreamsTestList.get(ListHelper.getMostCompactAudioIndex(MediaFormat.M4A, audioStreamsTestList)); + assertEquals(128, stream.average_bitrate); + assertEquals(MediaFormat.M4A, stream.getFormat()); + stream = audioStreamsTestList.get(ListHelper.getMostCompactAudioIndex(MediaFormat.WEBMA, audioStreamsTestList)); + assertEquals(64, stream.average_bitrate); + assertEquals(MediaFormat.WEBMA, stream.getFormat()); + + stream = audioStreamsTestList.get(ListHelper.getMostCompactAudioIndex(MediaFormat.MP3, audioStreamsTestList)); + assertEquals(64, stream.average_bitrate); + assertEquals(MediaFormat.MP3, stream.getFormat()); + } + + @Test + public void getLowestQualityAudioFormatPreferredAbsent() throws Exception { + + ////////////////////////////////////////// + // Doesn't contain the preferred format // + //////////////////////////////////////// + + List testList = new ArrayList<>(Arrays.asList( + new AudioStream("", MediaFormat.M4A, /**/ 128), + new AudioStream("", MediaFormat.WEBMA, /**/ 192))); + // List doesn't contains this format, it should fallback to the most compact audio no matter what format it is. + AudioStream stream = testList.get(ListHelper.getMostCompactAudioIndex(MediaFormat.MP3, testList)); + assertEquals(128, stream.average_bitrate); + assertEquals(MediaFormat.M4A, stream.getFormat()); + + // WEBMA is more compact than M4A + testList.add(new AudioStream("", MediaFormat.WEBMA, /**/ 128)); + stream = testList.get(ListHelper.getMostCompactAudioIndex(MediaFormat.MP3, testList)); + assertEquals(128, stream.average_bitrate); + assertEquals(MediaFormat.WEBMA, stream.getFormat()); + + //////////////////////////////////////////////////////// + // Multiple not-preferred-formats and equal bitrates // + ////////////////////////////////////////////////////// + + testList = new ArrayList<>(Arrays.asList( + new AudioStream("", MediaFormat.WEBMA, /**/ 192), + new AudioStream("", MediaFormat.M4A, /**/ 192), + new AudioStream("", MediaFormat.WEBMA, /**/ 256), + new AudioStream("", MediaFormat.M4A, /**/ 192), + new AudioStream("", MediaFormat.WEBMA, /**/ 192), + new AudioStream("", MediaFormat.M4A, /**/ 192))); + // List doesn't contains this format, it should fallback to the most compact audio no matter what format it is. + stream = testList.get(ListHelper.getMostCompactAudioIndex(MediaFormat.MP3, testList)); + assertEquals(192, stream.average_bitrate); + assertEquals(MediaFormat.WEBMA, stream.getFormat()); + + // Should be same as above + stream = testList.get(ListHelper.getMostCompactAudioIndex(null, testList)); + assertEquals(192, stream.average_bitrate); + assertEquals(MediaFormat.WEBMA, stream.getFormat()); + } + + @Test + public void getLowestQualityAudioNull() throws Exception { + assertEquals(-1, ListHelper.getMostCompactAudioIndex(null, null)); + assertEquals(-1, ListHelper.getMostCompactAudioIndex(null, new ArrayList())); + } + + @Test + public void getVideoDefaultStreamIndexCombinations() throws Exception { + List testList = Arrays.asList( + new VideoStream("", MediaFormat.MPEG_4, /**/ "1080p"), + new VideoStream("", MediaFormat.MPEG_4, /**/ "720p60"), + new VideoStream("", MediaFormat.MPEG_4, /**/ "720p"), + new VideoStream("", MediaFormat.WEBM, /**/ "480p"), + new VideoStream("", MediaFormat.MPEG_4, /**/ "360p"), + new VideoStream("", MediaFormat.WEBM, /**/ "360p"), + new VideoStream("", MediaFormat.v3GPP, /**/ "240p60"), + new VideoStream("", MediaFormat.WEBM, /**/ "144p")); + + // exact matches + assertEquals(1, ListHelper.getVideoStreamIndex("720p60", MediaFormat.MPEG_4, testList)); + assertEquals(2, ListHelper.getVideoStreamIndex("720p", MediaFormat.MPEG_4, testList)); + + // match but not refresh + assertEquals(0, ListHelper.getVideoStreamIndex("1080p60", MediaFormat.MPEG_4, testList)); + assertEquals(6, ListHelper.getVideoStreamIndex("240p", MediaFormat.v3GPP, testList)); + + // match but not format + assertEquals(1, ListHelper.getVideoStreamIndex("720p60", MediaFormat.WEBM, testList)); + assertEquals(2, ListHelper.getVideoStreamIndex("720p", MediaFormat.WEBM, testList)); + assertEquals(1, ListHelper.getVideoStreamIndex("720p60", null, testList)); + assertEquals(2, ListHelper.getVideoStreamIndex("720p", null, testList)); + + // match but not format and not refresh + assertEquals(0, ListHelper.getVideoStreamIndex("1080p60", MediaFormat.WEBM, testList)); + assertEquals(6, ListHelper.getVideoStreamIndex("240p", MediaFormat.WEBM, testList)); + assertEquals(0, ListHelper.getVideoStreamIndex("1080p60", null, testList)); + assertEquals(6, ListHelper.getVideoStreamIndex("240p", null, testList)); + + // match closest lower resolution + assertEquals(7, ListHelper.getVideoStreamIndex("200p", MediaFormat.WEBM, testList)); + assertEquals(7, ListHelper.getVideoStreamIndex("200p60", MediaFormat.WEBM, testList)); + assertEquals(7, ListHelper.getVideoStreamIndex("200p", MediaFormat.MPEG_4, testList)); + assertEquals(7, ListHelper.getVideoStreamIndex("200p60", MediaFormat.MPEG_4, testList)); + assertEquals(7, ListHelper.getVideoStreamIndex("200p", null, testList)); + assertEquals(7, ListHelper.getVideoStreamIndex("200p60", null, testList)); + + // Can't find a match + assertEquals(-1, ListHelper.getVideoStreamIndex("100p", null, testList)); + } } \ No newline at end of file