From de7872d8f2301e7d95d8b6dd362a12cedc80c278 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Fri, 17 Mar 2023 21:51:40 +0100 Subject: [PATCH] feat: add audio language selector --- app/build.gradle | 2 +- .../org/schabi/newpipe/player/Player.java | 11 +++ .../player/mediaitem/MediaItemTag.java | 39 ++++++++ .../player/mediaitem/StreamInfoTag.java | 23 ++++- .../player/resolver/PlaybackResolver.java | 5 + .../resolver/VideoPlaybackResolver.java | 55 ++++++++--- .../newpipe/player/ui/VideoPlayerUi.java | 76 ++++++++++++++- .../org/schabi/newpipe/util/ListHelper.java | 94 +++++++++++++++++-- app/src/main/res/layout/player.xml | 16 ++++ 9 files changed, 295 insertions(+), 26 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index a5d63f429..c0d423817 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -191,7 +191,7 @@ dependencies { // name and the commit hash with the commit hash of the (pushed) commit you want to test // This works thanks to JitPack: https://jitpack.io/ implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751' - implementation 'com.github.TeamNewPipe:NewPipeExtractor:7e793c11aec46358ccbfd8bcfcf521105f4f093a' + implementation 'com.github.TeamNewPipe:NewPipeExtractor:5a9b6ed2e3306b9152cc6689dd61dbbe43483845' implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0' /** Checkstyle **/ 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 4243c233b..69b161631 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -88,6 +88,7 @@ import org.schabi.newpipe.databinding.PlayerBinding; import org.schabi.newpipe.error.ErrorInfo; import org.schabi.newpipe.error.ErrorUtil; import org.schabi.newpipe.error.UserAction; +import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.extractor.stream.VideoStream; @@ -1886,6 +1887,12 @@ public final class Player implements PlaybackListener, Listener { .map(quality -> quality.getSortedVideoStreams() .get(quality.getSelectedVideoStreamIndex())); } + + public Optional getSelectedAudioStream() { + return Optional.ofNullable(currentMetadata) + .flatMap(MediaItemTag::getMaybeAudioLanguage) + .map(MediaItemTag.AudioLanguage::getSelectedAudioStream); + } //endregion @@ -2178,6 +2185,10 @@ public final class Player implements PlaybackListener, Listener { videoResolver.setPlaybackQuality(quality); } + public void setAudioLanguage(@Nullable final String language) { + videoResolver.setAudioLanguage(language); + } + @NonNull public Context getContext() { diff --git a/app/src/main/java/org/schabi/newpipe/player/mediaitem/MediaItemTag.java b/app/src/main/java/org/schabi/newpipe/player/mediaitem/MediaItemTag.java index f08086287..0ef1eaaf1 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediaitem/MediaItemTag.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediaitem/MediaItemTag.java @@ -7,6 +7,7 @@ import com.google.android.exoplayer2.MediaItem.RequestMetadata; import com.google.android.exoplayer2.MediaMetadata; import com.google.android.exoplayer2.Player; +import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.extractor.stream.VideoStream; @@ -55,6 +56,11 @@ public interface MediaItemTag { return Optional.empty(); } + @NonNull + default Optional getMaybeAudioLanguage() { + return Optional.empty(); + } + Optional getMaybeExtras(@NonNull Class type); MediaItemTag withExtras(@NonNull T extra); @@ -128,4 +134,37 @@ public interface MediaItemTag { ? null : sortedVideoStreams.get(selectedVideoStreamIndex); } } + + final class AudioLanguage { + @NonNull + private final List audioStreams; + private final int selectedAudioStreamIndex; + + private AudioLanguage(@NonNull final List audioStreams, + final int selectedAudioStreamIndex) { + this.audioStreams = audioStreams; + this.selectedAudioStreamIndex = selectedAudioStreamIndex; + } + + static AudioLanguage of(@NonNull final List audioStreams, + final int selectedAudioStreamIndex) { + return new AudioLanguage(audioStreams, selectedAudioStreamIndex); + } + + @NonNull + public List getAudioStreams() { + return audioStreams; + } + + public int getSelectedAudioStreamIndex() { + return selectedAudioStreamIndex; + } + + @Nullable + public AudioStream getSelectedAudioStream() { + return selectedAudioStreamIndex < 0 + || selectedAudioStreamIndex >= audioStreams.size() + ? null : audioStreams.get(selectedAudioStreamIndex); + } + } } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediaitem/StreamInfoTag.java b/app/src/main/java/org/schabi/newpipe/player/mediaitem/StreamInfoTag.java index 4095f2bc8..5379fc5a6 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediaitem/StreamInfoTag.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediaitem/StreamInfoTag.java @@ -2,6 +2,7 @@ package org.schabi.newpipe.player.mediaitem; import com.google.android.exoplayer2.MediaItem; +import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.extractor.stream.VideoStream; @@ -25,25 +26,33 @@ public final class StreamInfoTag implements MediaItemTag { @Nullable private final MediaItemTag.Quality quality; @Nullable + private final MediaItemTag.AudioLanguage audioLanguage; + @Nullable private final Object extras; private StreamInfoTag(@NonNull final StreamInfo streamInfo, @Nullable final MediaItemTag.Quality quality, + @Nullable final MediaItemTag.AudioLanguage audioLanguage, @Nullable final Object extras) { this.streamInfo = streamInfo; this.quality = quality; + this.audioLanguage = audioLanguage; this.extras = extras; } public static StreamInfoTag of(@NonNull final StreamInfo streamInfo, @NonNull final List sortedVideoStreams, - final int selectedVideoStreamIndex) { + final int selectedVideoStreamIndex, + @NonNull final List audioStreams, + final int selectedAudioStreamIndex) { final Quality quality = Quality.of(sortedVideoStreams, selectedVideoStreamIndex); - return new StreamInfoTag(streamInfo, quality, null); + final AudioLanguage audioLanguage = + AudioLanguage.of(audioStreams, selectedAudioStreamIndex); + return new StreamInfoTag(streamInfo, quality, audioLanguage, null); } public static StreamInfoTag of(@NonNull final StreamInfo streamInfo) { - return new StreamInfoTag(streamInfo, null, null); + return new StreamInfoTag(streamInfo, null, null, null); } @Override @@ -103,6 +112,12 @@ public final class StreamInfoTag implements MediaItemTag { return Optional.ofNullable(quality); } + @NonNull + @Override + public Optional getMaybeAudioLanguage() { + return Optional.ofNullable(audioLanguage); + } + @Override public Optional getMaybeExtras(@NonNull final Class type) { return Optional.ofNullable(extras).map(type::cast); @@ -110,6 +125,6 @@ public final class StreamInfoTag implements MediaItemTag { @Override public StreamInfoTag withExtras(@NonNull final Object extra) { - return new StreamInfoTag(streamInfo, quality, extra); + return new StreamInfoTag(streamInfo, quality, audioLanguage, extra); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java index 9c8cbb8f6..c15447418 100644 --- a/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java +++ b/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java @@ -156,6 +156,11 @@ public interface PlaybackResolver extends Resolver { cacheKey.append(audioStream.getAverageBitrate()); } + if (audioStream.getAudioTrackId() != null) { + cacheKey.append(" "); + cacheKey.append(audioStream.getAudioTrackId()); + } + return cacheKey.toString(); } 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 cf7d73558..2f0c59325 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 @@ -28,6 +28,7 @@ import java.util.List; import java.util.Optional; import static com.google.android.exoplayer2.C.TIME_UNSET; +import static org.schabi.newpipe.util.ListHelper.getFilteredAudioStreams; import static org.schabi.newpipe.util.ListHelper.getUrlAndNonTorrentStreams; import static org.schabi.newpipe.util.ListHelper.getNonTorrentStreams; @@ -44,6 +45,8 @@ public class VideoPlaybackResolver implements PlaybackResolver { @Nullable private String playbackQuality; + @Nullable + private String audioLanguage; public enum SourceType { LIVE_STREAM, @@ -74,19 +77,39 @@ public class VideoPlaybackResolver implements PlaybackResolver { final List videoStreamsList = ListHelper.getSortedStreamVideosList(context, getNonTorrentStreams(info.getVideoStreams()), getNonTorrentStreams(info.getVideoOnlyStreams()), false, true); - final int index; + final List audioStreamsList = + getFilteredAudioStreams(context, info.getAudioStreams()); + + final int videoIndex; if (videoStreamsList.isEmpty()) { - index = -1; + videoIndex = -1; } else if (playbackQuality == null) { - index = qualityResolver.getDefaultResolutionIndex(videoStreamsList); + videoIndex = qualityResolver.getDefaultResolutionIndex(videoStreamsList); } else { - index = qualityResolver.getOverrideResolutionIndex(videoStreamsList, + videoIndex = qualityResolver.getOverrideResolutionIndex(videoStreamsList, getPlaybackQuality()); } - final MediaItemTag tag = StreamInfoTag.of(info, videoStreamsList, index); + + int audioIndex = 0; + if (audioLanguage != null) { + for (int i = 0; i < audioStreamsList.size(); i++) { + final AudioStream stream = audioStreamsList.get(i); + if (stream.getAudioTrackId() != null + && stream.getAudioTrackId().equals(audioLanguage)) { + audioIndex = i; + break; + } + } + } + + final MediaItemTag tag = + StreamInfoTag.of(info, videoStreamsList, videoIndex, audioStreamsList, audioIndex); @Nullable final VideoStream video = tag.getMaybeQuality() .map(MediaItemTag.Quality::getSelectedVideoStream) .orElse(null); + @Nullable final AudioStream audio = tag.getMaybeAudioLanguage() + .map(MediaItemTag.AudioLanguage::getSelectedAudioStream) + .orElse(null); if (video != null) { try { @@ -99,14 +122,9 @@ public class VideoPlaybackResolver implements PlaybackResolver { } } - // Create optional audio stream source - final List audioStreams = getNonTorrentStreams(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())) { + if (audio != null && (video == null || video.isVideoOnly() || audioLanguage != null)) { try { final MediaSource audioSource = PlaybackResolver.buildMediaSource( dataSource, audio, info, PlaybackResolver.cacheKeyOf(info, audio), tag); @@ -179,9 +197,24 @@ public class VideoPlaybackResolver implements PlaybackResolver { this.playbackQuality = playbackQuality; } + @Nullable + public String getAudioLanguage() { + return audioLanguage; + } + + public void setAudioLanguage(@Nullable final String audioLanguage) { + this.audioLanguage = audioLanguage; + } + public interface QualityResolver { int getDefaultResolutionIndex(List sortedVideos); int getOverrideResolutionIndex(List sortedVideos, String playbackQuality); } + + public interface AudioLanguageResolver { + int getDefaultLanguageIndex(List audioStreams); + + int getOverrideLanguageIndex(List audioStreams, String audioLanguage); + } } diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java index 9afd1bf24..3365ade72 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java +++ b/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java @@ -63,6 +63,7 @@ import org.schabi.newpipe.App; import org.schabi.newpipe.R; import org.schabi.newpipe.databinding.PlayerBinding; import org.schabi.newpipe.extractor.MediaFormat; +import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.fragments.detail.VideoDetailFragment; @@ -117,11 +118,13 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa //////////////////////////////////////////////////////////////////////////*/ private static final int POPUP_MENU_ID_QUALITY = 69; + private static final int POPUP_MENU_ID_LANGUAGE = 70; private static final int POPUP_MENU_ID_PLAYBACK_SPEED = 79; private static final int POPUP_MENU_ID_CAPTION = 89; protected boolean isSomePopupMenuVisible = false; private PopupMenu qualityPopupMenu; + private PopupMenu languagePopupMenu; protected PopupMenu playbackSpeedPopupMenu; private PopupMenu captionPopupMenu; @@ -146,7 +149,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa //region Constructor, setup, destroy protected VideoPlayerUi(@NonNull final Player player, - @NonNull final PlayerBinding playerBinding) { + @NonNull final PlayerBinding playerBinding) { super(player); binding = playerBinding; setupFromView(); @@ -173,6 +176,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa R.style.DarkPopupMenu); qualityPopupMenu = new PopupMenu(themeWrapper, binding.qualityTextView); + languagePopupMenu = new PopupMenu(themeWrapper, binding.languageTextView); playbackSpeedPopupMenu = new PopupMenu(context, binding.playbackSpeed); captionPopupMenu = new PopupMenu(themeWrapper, binding.captionTextView); @@ -190,6 +194,8 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa protected void initListeners() { binding.qualityTextView.setOnClickListener(makeOnClickListener(this::onQualityClicked)); + binding.languageTextView.setOnClickListener( + makeOnClickListener(this::onAudioLanguageClicked)); binding.playbackSpeed.setOnClickListener(makeOnClickListener(this::onPlaybackSpeedClicked)); binding.playbackSeekBar.setOnSeekBarChangeListener(this); @@ -266,6 +272,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa protected void deinitListeners() { binding.qualityTextView.setOnClickListener(null); + binding.languageTextView.setOnClickListener(null); binding.playbackSpeed.setOnClickListener(null); binding.playbackSeekBar.setOnSeekBarChangeListener(null); binding.captionTextView.setOnClickListener(null); @@ -419,6 +426,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa binding.topControls.setPaddingRelative(controlsPad, playerTopPad, controlsPad, 0); binding.bottomControls.setPaddingRelative(controlsPad, 0, controlsPad, 0); binding.qualityTextView.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad); + binding.languageTextView.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad); binding.playbackSpeed.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad); binding.playbackSpeed.setMinimumWidth(buttonsMinWidth); binding.captionTextView.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad); @@ -984,6 +992,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa private void updateStreamRelatedViews() { player.getCurrentStreamInfo().ifPresent(info -> { binding.qualityTextView.setVisibility(View.GONE); + binding.languageTextView.setVisibility(View.GONE); binding.playbackSpeed.setVisibility(View.GONE); binding.playbackEndTime.setVisibility(View.GONE); @@ -1019,6 +1028,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa } buildQualityMenu(); + buildLanguageMenu(); binding.qualityTextView.setVisibility(View.VISIBLE); binding.surfaceView.setVisibility(View.VISIBLE); @@ -1067,6 +1077,37 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa .ifPresent(s -> binding.qualityTextView.setText(s.getResolution())); } + private void buildLanguageMenu() { + if (languagePopupMenu == null) { + return; + } + languagePopupMenu.getMenu().removeGroup(POPUP_MENU_ID_LANGUAGE); + + final List availableStreams = Optional.ofNullable(player.getCurrentMetadata()) + .flatMap(MediaItemTag::getMaybeAudioLanguage) + .map(MediaItemTag.AudioLanguage::getAudioStreams) + .orElse(null); + if (availableStreams == null || availableStreams.size() < 2) { + return; + } + + for (int i = 0; i < availableStreams.size(); i++) { + final AudioStream audioStream = availableStreams.get(i); + // TODO: ensure that audio streams have track names + if (audioStream.getAudioTrackName() == null) { + continue; + } + languagePopupMenu.getMenu().add(POPUP_MENU_ID_LANGUAGE, i, Menu.NONE, + audioStream.getAudioTrackName()); + } + + player.getSelectedAudioStream() + .ifPresent(s -> binding.languageTextView.setText(s.getAudioTrackName())); + binding.languageTextView.setVisibility(View.VISIBLE); + languagePopupMenu.setOnMenuItemClickListener(this); + languagePopupMenu.setOnDismissListener(this); + } + private void buildPlaybackSpeedMenu() { if (playbackSpeedPopupMenu == null) { return; @@ -1175,6 +1216,15 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa .ifPresent(binding.qualityTextView::setText); } + private void onAudioLanguageClicked() { + languagePopupMenu.show(); + isSomePopupMenuVisible = true; + + player.getSelectedAudioStream() + .map(AudioStream::getAudioTrackName) + .ifPresent(binding.languageTextView::setText); + } + /** * Called when an item of the quality selector or the playback speed selector is selected. */ @@ -1208,6 +1258,30 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa binding.qualityTextView.setText(menuItem.getTitle()); return true; + } else if (menuItem.getGroupId() == POPUP_MENU_ID_LANGUAGE) { + final int menuItemIndex = menuItem.getItemId(); + @Nullable final MediaItemTag currentMetadata = player.getCurrentMetadata(); + //noinspection SimplifyOptionalCallChains + if (currentMetadata == null || !currentMetadata.getMaybeAudioLanguage().isPresent()) { + return true; + } + + final MediaItemTag.AudioLanguage language = + currentMetadata.getMaybeAudioLanguage().get(); + final List availableStreams = language.getAudioStreams(); + final int selectedStreamIndex = language.getSelectedAudioStreamIndex(); + if (selectedStreamIndex == menuItemIndex || availableStreams.size() <= menuItemIndex) { + return true; + } + + player.saveStreamProgressState(); + final String newLanguage = availableStreams.get(menuItemIndex).getAudioTrackId(); + player.setRecovery(); + player.setAudioLanguage(newLanguage); + player.reloadPlayQueueManager(); + + binding.languageTextView.setText(menuItem.getTitle()); + return true; } else if (menuItem.getGroupId() == POPUP_MENU_ID_PLAYBACK_SPEED) { final int speedIndex = menuItem.getItemId(); final float speed = PLAYBACK_SPEEDS[speedIndex]; 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 b3b7c1792..ed1e1e801 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java @@ -23,6 +23,7 @@ import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Objects; import java.util.Set; import java.util.function.Predicate; @@ -42,13 +43,14 @@ public final class ListHelper { // Use a Set for better performance private static final Set HIGH_RESOLUTION_LIST = Set.of("1440p", "2160p"); - private ListHelper() { } + private ListHelper() { + } /** - * @see #getDefaultResolutionIndex(String, String, MediaFormat, List) * @param context Android app context * @param videoStreams list of the video streams to check * @return index of the video stream with the default index + * @see #getDefaultResolutionIndex(String, String, MediaFormat, List) */ public static int getDefaultResolutionIndex(final Context context, final List videoStreams) { @@ -58,11 +60,11 @@ public final class ListHelper { } /** - * @see #getDefaultResolutionIndex(String, String, MediaFormat, List) * @param context Android app context * @param videoStreams list of the video streams to check * @param defaultResolution the default resolution to look for * @return index of the video stream with the default index + * @see #getDefaultResolutionIndex(String, String, MediaFormat, List) */ public static int getResolutionIndex(final Context context, final List videoStreams, @@ -71,10 +73,10 @@ public final class ListHelper { } /** - * @see #getDefaultResolutionIndex(String, String, MediaFormat, List) - * @param context Android app context - * @param videoStreams list of the video streams to check + * @param context Android app context + * @param videoStreams list of the video streams to check * @return index of the video stream with the default index + * @see #getDefaultResolutionIndex(String, String, MediaFormat, List) */ public static int getPopupDefaultResolutionIndex(final Context context, final List videoStreams) { @@ -84,11 +86,11 @@ public final class ListHelper { } /** - * @see #getDefaultResolutionIndex(String, String, MediaFormat, List) * @param context Android app context * @param videoStreams list of the video streams to check * @param defaultResolution the default resolution to look for * @return index of the video stream with the default index + * @see #getDefaultResolutionIndex(String, String, MediaFormat, List) */ public static int getPopupResolutionIndex(final Context context, final List videoStreams, @@ -186,6 +188,80 @@ public final class ListHelper { videoOnlyStreams, ascendingOrder, preferVideoOnlyStreams); } + public static List getFilteredAudioStreams( + @NonNull final Context context, + @Nullable final List audioStreams) { + if (audioStreams == null) { + return Collections.emptyList(); + } + + final HashMap collectedStreams = new HashMap<>(); + + final Comparator cmp; + if (isLimitingDataUsage(context)) { + cmp = getAudioStreamComparator(AUDIO_FORMAT_EFFICIENCY_RANKING); + } else { + cmp = getAudioStreamComparator(AUDIO_FORMAT_QUALITY_RANKING); + } + + final String preferredLanguage = Localization.getPreferredLocale(context).getISO3Language(); + boolean hasPreferredLanguage = false; + + for (final AudioStream stream : audioStreams) { + if (stream.getDeliveryMethod() == DeliveryMethod.TORRENT) { + continue; + } + + final String trackId; + if (stream.getAudioTrackId() != null) { + trackId = stream.getAudioTrackId(); + } else { + trackId = ""; + } + + final AudioStream presentStream = collectedStreams.get(trackId); + if (presentStream == null || cmp.compare(stream, presentStream) > 0) { + collectedStreams.put(trackId, stream); + + if (stream.getAudioLocale() != null + && stream.getAudioLocale().getISO3Language().equals(preferredLanguage)) { + hasPreferredLanguage = true; + } + } + } + + // Fall back to English if the preferred language was not found + final String preferredLanguageOrEnglish = + hasPreferredLanguage ? preferredLanguage : Locale.ENGLISH.getISO3Language(); + + // Sort collected streams + return collectedStreams.values().stream() + .sorted((s1, s2) -> { + // Preferred language comes first + if (s1.getAudioLocale() != null + && s1.getAudioLocale().getISO3Language() + .equals(preferredLanguageOrEnglish)) { + return -1; + } + if (s2.getAudioLocale() != null + && s2.getAudioLocale().getISO3Language() + .equals(preferredLanguageOrEnglish)) { + return 1; + } + + // Sort audio tracks alphabetically + if (s1.getAudioTrackName() != null) { + if (s2.getAudioTrackName() != null) { + return s1.getAudioTrackName().compareTo(s2.getAudioTrackName()); + } else { + return -1; + } + } + return 1; + }) + .collect(Collectors.toList()); + } + /*////////////////////////////////////////////////////////////////////////// // Utils //////////////////////////////////////////////////////////////////////////*/ @@ -300,8 +376,8 @@ public final class ListHelper { // Filter out higher resolutions (or not if high resolutions should always be shown) .filter(stream -> showHigherResolutions || !HIGH_RESOLUTION_LIST.contains(stream.getResolution() - // Replace any frame rate with nothing - .replaceAll("p\\d+$", "p"))) + // Replace any frame rate with nothing + .replaceAll("p\\d+$", "p"))) .collect(Collectors.toList()); final HashMap hashMap = new HashMap<>(); diff --git a/app/src/main/res/layout/player.xml b/app/src/main/res/layout/player.xml index b528e4e9b..82760be3a 100644 --- a/app/src/main/res/layout/player.xml +++ b/app/src/main/res/layout/player.xml @@ -157,6 +157,22 @@ tools:text="The Video Artist LONG very LONG very Long" /> + +