mirror of
				https://github.com/TeamNewPipe/NewPipe
				synced 2025-10-31 15:23:00 +00:00 
			
		
		
		
	feat: add audio language selector
This commit is contained in:
		| @@ -191,7 +191,7 @@ dependencies { | |||||||
|     // name and the commit hash with the commit hash of the (pushed) commit you want to test |     // 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/ |     // This works thanks to JitPack: https://jitpack.io/ | ||||||
|     implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751' |     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' |     implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0' | ||||||
|  |  | ||||||
| /** Checkstyle **/ | /** Checkstyle **/ | ||||||
|   | |||||||
| @@ -88,6 +88,7 @@ import org.schabi.newpipe.databinding.PlayerBinding; | |||||||
| import org.schabi.newpipe.error.ErrorInfo; | import org.schabi.newpipe.error.ErrorInfo; | ||||||
| import org.schabi.newpipe.error.ErrorUtil; | import org.schabi.newpipe.error.ErrorUtil; | ||||||
| import org.schabi.newpipe.error.UserAction; | 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.StreamInfo; | ||||||
| import org.schabi.newpipe.extractor.stream.StreamType; | import org.schabi.newpipe.extractor.stream.StreamType; | ||||||
| import org.schabi.newpipe.extractor.stream.VideoStream; | import org.schabi.newpipe.extractor.stream.VideoStream; | ||||||
| @@ -1886,6 +1887,12 @@ public final class Player implements PlaybackListener, Listener { | |||||||
|                 .map(quality -> quality.getSortedVideoStreams() |                 .map(quality -> quality.getSortedVideoStreams() | ||||||
|                         .get(quality.getSelectedVideoStreamIndex())); |                         .get(quality.getSelectedVideoStreamIndex())); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     public Optional<AudioStream> getSelectedAudioStream() { | ||||||
|  |         return Optional.ofNullable(currentMetadata) | ||||||
|  |                 .flatMap(MediaItemTag::getMaybeAudioLanguage) | ||||||
|  |                 .map(MediaItemTag.AudioLanguage::getSelectedAudioStream); | ||||||
|  |     } | ||||||
|     //endregion |     //endregion | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -2178,6 +2185,10 @@ public final class Player implements PlaybackListener, Listener { | |||||||
|         videoResolver.setPlaybackQuality(quality); |         videoResolver.setPlaybackQuality(quality); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     public void setAudioLanguage(@Nullable final String language) { | ||||||
|  |         videoResolver.setAudioLanguage(language); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|     @NonNull |     @NonNull | ||||||
|     public Context getContext() { |     public Context getContext() { | ||||||
|   | |||||||
| @@ -7,6 +7,7 @@ import com.google.android.exoplayer2.MediaItem.RequestMetadata; | |||||||
| import com.google.android.exoplayer2.MediaMetadata; | import com.google.android.exoplayer2.MediaMetadata; | ||||||
| import com.google.android.exoplayer2.Player; | 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.StreamInfo; | ||||||
| import org.schabi.newpipe.extractor.stream.StreamType; | import org.schabi.newpipe.extractor.stream.StreamType; | ||||||
| import org.schabi.newpipe.extractor.stream.VideoStream; | import org.schabi.newpipe.extractor.stream.VideoStream; | ||||||
| @@ -55,6 +56,11 @@ public interface MediaItemTag { | |||||||
|         return Optional.empty(); |         return Optional.empty(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @NonNull | ||||||
|  |     default Optional<AudioLanguage> getMaybeAudioLanguage() { | ||||||
|  |         return Optional.empty(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     <T> Optional<T> getMaybeExtras(@NonNull Class<T> type); |     <T> Optional<T> getMaybeExtras(@NonNull Class<T> type); | ||||||
|  |  | ||||||
|     <T> MediaItemTag withExtras(@NonNull T extra); |     <T> MediaItemTag withExtras(@NonNull T extra); | ||||||
| @@ -128,4 +134,37 @@ public interface MediaItemTag { | |||||||
|                     ? null : sortedVideoStreams.get(selectedVideoStreamIndex); |                     ? null : sortedVideoStreams.get(selectedVideoStreamIndex); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     final class AudioLanguage { | ||||||
|  |         @NonNull | ||||||
|  |         private final List<AudioStream> audioStreams; | ||||||
|  |         private final int selectedAudioStreamIndex; | ||||||
|  |  | ||||||
|  |         private AudioLanguage(@NonNull final List<AudioStream> audioStreams, | ||||||
|  |                         final int selectedAudioStreamIndex) { | ||||||
|  |             this.audioStreams = audioStreams; | ||||||
|  |             this.selectedAudioStreamIndex = selectedAudioStreamIndex; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         static AudioLanguage of(@NonNull final List<AudioStream> audioStreams, | ||||||
|  |                                 final int selectedAudioStreamIndex) { | ||||||
|  |             return new AudioLanguage(audioStreams, selectedAudioStreamIndex); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         @NonNull | ||||||
|  |         public List<AudioStream> getAudioStreams() { | ||||||
|  |             return audioStreams; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public int getSelectedAudioStreamIndex() { | ||||||
|  |             return selectedAudioStreamIndex; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         @Nullable | ||||||
|  |         public AudioStream getSelectedAudioStream() { | ||||||
|  |             return selectedAudioStreamIndex < 0 | ||||||
|  |                     || selectedAudioStreamIndex >= audioStreams.size() | ||||||
|  |                     ? null : audioStreams.get(selectedAudioStreamIndex); | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ package org.schabi.newpipe.player.mediaitem; | |||||||
|  |  | ||||||
| import com.google.android.exoplayer2.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.StreamInfo; | ||||||
| import org.schabi.newpipe.extractor.stream.StreamType; | import org.schabi.newpipe.extractor.stream.StreamType; | ||||||
| import org.schabi.newpipe.extractor.stream.VideoStream; | import org.schabi.newpipe.extractor.stream.VideoStream; | ||||||
| @@ -25,25 +26,33 @@ public final class StreamInfoTag implements MediaItemTag { | |||||||
|     @Nullable |     @Nullable | ||||||
|     private final MediaItemTag.Quality quality; |     private final MediaItemTag.Quality quality; | ||||||
|     @Nullable |     @Nullable | ||||||
|  |     private final MediaItemTag.AudioLanguage audioLanguage; | ||||||
|  |     @Nullable | ||||||
|     private final Object extras; |     private final Object extras; | ||||||
|  |  | ||||||
|     private StreamInfoTag(@NonNull final StreamInfo streamInfo, |     private StreamInfoTag(@NonNull final StreamInfo streamInfo, | ||||||
|                           @Nullable final MediaItemTag.Quality quality, |                           @Nullable final MediaItemTag.Quality quality, | ||||||
|  |                           @Nullable final MediaItemTag.AudioLanguage audioLanguage, | ||||||
|                           @Nullable final Object extras) { |                           @Nullable final Object extras) { | ||||||
|         this.streamInfo = streamInfo; |         this.streamInfo = streamInfo; | ||||||
|         this.quality = quality; |         this.quality = quality; | ||||||
|  |         this.audioLanguage = audioLanguage; | ||||||
|         this.extras = extras; |         this.extras = extras; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public static StreamInfoTag of(@NonNull final StreamInfo streamInfo, |     public static StreamInfoTag of(@NonNull final StreamInfo streamInfo, | ||||||
|                                    @NonNull final List<VideoStream> sortedVideoStreams, |                                    @NonNull final List<VideoStream> sortedVideoStreams, | ||||||
|                                    final int selectedVideoStreamIndex) { |                                    final int selectedVideoStreamIndex, | ||||||
|  |                                    @NonNull final List<AudioStream> audioStreams, | ||||||
|  |                                    final int selectedAudioStreamIndex) { | ||||||
|         final Quality quality = Quality.of(sortedVideoStreams, selectedVideoStreamIndex); |         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) { |     public static StreamInfoTag of(@NonNull final StreamInfo streamInfo) { | ||||||
|         return new StreamInfoTag(streamInfo, null, null); |         return new StreamInfoTag(streamInfo, null, null, null); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
| @@ -103,6 +112,12 @@ public final class StreamInfoTag implements MediaItemTag { | |||||||
|         return Optional.ofNullable(quality); |         return Optional.ofNullable(quality); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @NonNull | ||||||
|  |     @Override | ||||||
|  |     public Optional<AudioLanguage> getMaybeAudioLanguage() { | ||||||
|  |         return Optional.ofNullable(audioLanguage); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public <T> Optional<T> getMaybeExtras(@NonNull final Class<T> type) { |     public <T> Optional<T> getMaybeExtras(@NonNull final Class<T> type) { | ||||||
|         return Optional.ofNullable(extras).map(type::cast); |         return Optional.ofNullable(extras).map(type::cast); | ||||||
| @@ -110,6 +125,6 @@ public final class StreamInfoTag implements MediaItemTag { | |||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public StreamInfoTag withExtras(@NonNull final Object extra) { |     public StreamInfoTag withExtras(@NonNull final Object extra) { | ||||||
|         return new StreamInfoTag(streamInfo, quality, extra); |         return new StreamInfoTag(streamInfo, quality, audioLanguage, extra); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -156,6 +156,11 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> { | |||||||
|             cacheKey.append(audioStream.getAverageBitrate()); |             cacheKey.append(audioStream.getAverageBitrate()); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         if (audioStream.getAudioTrackId() != null) { | ||||||
|  |             cacheKey.append(" "); | ||||||
|  |             cacheKey.append(audioStream.getAudioTrackId()); | ||||||
|  |         } | ||||||
|  |  | ||||||
|         return cacheKey.toString(); |         return cacheKey.toString(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -28,6 +28,7 @@ import java.util.List; | |||||||
| import java.util.Optional; | import java.util.Optional; | ||||||
|  |  | ||||||
| import static com.google.android.exoplayer2.C.TIME_UNSET; | 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.getUrlAndNonTorrentStreams; | ||||||
| import static org.schabi.newpipe.util.ListHelper.getNonTorrentStreams; | import static org.schabi.newpipe.util.ListHelper.getNonTorrentStreams; | ||||||
|  |  | ||||||
| @@ -44,6 +45,8 @@ public class VideoPlaybackResolver implements PlaybackResolver { | |||||||
|  |  | ||||||
|     @Nullable |     @Nullable | ||||||
|     private String playbackQuality; |     private String playbackQuality; | ||||||
|  |     @Nullable | ||||||
|  |     private String audioLanguage; | ||||||
|  |  | ||||||
|     public enum SourceType { |     public enum SourceType { | ||||||
|         LIVE_STREAM, |         LIVE_STREAM, | ||||||
| @@ -74,19 +77,39 @@ public class VideoPlaybackResolver implements PlaybackResolver { | |||||||
|         final List<VideoStream> videoStreamsList = ListHelper.getSortedStreamVideosList(context, |         final List<VideoStream> videoStreamsList = ListHelper.getSortedStreamVideosList(context, | ||||||
|                 getNonTorrentStreams(info.getVideoStreams()), |                 getNonTorrentStreams(info.getVideoStreams()), | ||||||
|                 getNonTorrentStreams(info.getVideoOnlyStreams()), false, true); |                 getNonTorrentStreams(info.getVideoOnlyStreams()), false, true); | ||||||
|         final int index; |         final List<AudioStream> audioStreamsList = | ||||||
|  |                 getFilteredAudioStreams(context, info.getAudioStreams()); | ||||||
|  |  | ||||||
|  |         final int videoIndex; | ||||||
|         if (videoStreamsList.isEmpty()) { |         if (videoStreamsList.isEmpty()) { | ||||||
|             index = -1; |             videoIndex = -1; | ||||||
|         } else if (playbackQuality == null) { |         } else if (playbackQuality == null) { | ||||||
|             index = qualityResolver.getDefaultResolutionIndex(videoStreamsList); |             videoIndex = qualityResolver.getDefaultResolutionIndex(videoStreamsList); | ||||||
|         } else { |         } else { | ||||||
|             index = qualityResolver.getOverrideResolutionIndex(videoStreamsList, |             videoIndex = qualityResolver.getOverrideResolutionIndex(videoStreamsList, | ||||||
|                     getPlaybackQuality()); |                     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() |         @Nullable final VideoStream video = tag.getMaybeQuality() | ||||||
|                 .map(MediaItemTag.Quality::getSelectedVideoStream) |                 .map(MediaItemTag.Quality::getSelectedVideoStream) | ||||||
|                 .orElse(null); |                 .orElse(null); | ||||||
|  |         @Nullable final AudioStream audio = tag.getMaybeAudioLanguage() | ||||||
|  |                 .map(MediaItemTag.AudioLanguage::getSelectedAudioStream) | ||||||
|  |                 .orElse(null); | ||||||
|  |  | ||||||
|         if (video != null) { |         if (video != null) { | ||||||
|             try { |             try { | ||||||
| @@ -99,14 +122,9 @@ public class VideoPlaybackResolver implements PlaybackResolver { | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // Create optional audio stream source |  | ||||||
|         final List<AudioStream> 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 |         // Use the audio stream if there is no video stream, or | ||||||
|         // merge with audio stream in case if video does not contain audio |         // 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 { |             try { | ||||||
|                 final MediaSource audioSource = PlaybackResolver.buildMediaSource( |                 final MediaSource audioSource = PlaybackResolver.buildMediaSource( | ||||||
|                         dataSource, audio, info, PlaybackResolver.cacheKeyOf(info, audio), tag); |                         dataSource, audio, info, PlaybackResolver.cacheKeyOf(info, audio), tag); | ||||||
| @@ -179,9 +197,24 @@ public class VideoPlaybackResolver implements PlaybackResolver { | |||||||
|         this.playbackQuality = playbackQuality; |         this.playbackQuality = playbackQuality; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @Nullable | ||||||
|  |     public String getAudioLanguage() { | ||||||
|  |         return audioLanguage; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void setAudioLanguage(@Nullable final String audioLanguage) { | ||||||
|  |         this.audioLanguage = audioLanguage; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     public interface QualityResolver { |     public interface QualityResolver { | ||||||
|         int getDefaultResolutionIndex(List<VideoStream> sortedVideos); |         int getDefaultResolutionIndex(List<VideoStream> sortedVideos); | ||||||
|  |  | ||||||
|         int getOverrideResolutionIndex(List<VideoStream> sortedVideos, String playbackQuality); |         int getOverrideResolutionIndex(List<VideoStream> sortedVideos, String playbackQuality); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     public interface AudioLanguageResolver { | ||||||
|  |         int getDefaultLanguageIndex(List<AudioStream> audioStreams); | ||||||
|  |  | ||||||
|  |         int getOverrideLanguageIndex(List<AudioStream> audioStreams, String audioLanguage); | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -63,6 +63,7 @@ import org.schabi.newpipe.App; | |||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
| import org.schabi.newpipe.databinding.PlayerBinding; | import org.schabi.newpipe.databinding.PlayerBinding; | ||||||
| import org.schabi.newpipe.extractor.MediaFormat; | 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.StreamInfo; | ||||||
| import org.schabi.newpipe.extractor.stream.VideoStream; | import org.schabi.newpipe.extractor.stream.VideoStream; | ||||||
| import org.schabi.newpipe.fragments.detail.VideoDetailFragment; | 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_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_PLAYBACK_SPEED = 79; | ||||||
|     private static final int POPUP_MENU_ID_CAPTION = 89; |     private static final int POPUP_MENU_ID_CAPTION = 89; | ||||||
|  |  | ||||||
|     protected boolean isSomePopupMenuVisible = false; |     protected boolean isSomePopupMenuVisible = false; | ||||||
|     private PopupMenu qualityPopupMenu; |     private PopupMenu qualityPopupMenu; | ||||||
|  |     private PopupMenu languagePopupMenu; | ||||||
|     protected PopupMenu playbackSpeedPopupMenu; |     protected PopupMenu playbackSpeedPopupMenu; | ||||||
|     private PopupMenu captionPopupMenu; |     private PopupMenu captionPopupMenu; | ||||||
|  |  | ||||||
| @@ -173,6 +176,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa | |||||||
|                 R.style.DarkPopupMenu); |                 R.style.DarkPopupMenu); | ||||||
|  |  | ||||||
|         qualityPopupMenu = new PopupMenu(themeWrapper, binding.qualityTextView); |         qualityPopupMenu = new PopupMenu(themeWrapper, binding.qualityTextView); | ||||||
|  |         languagePopupMenu = new PopupMenu(themeWrapper, binding.languageTextView); | ||||||
|         playbackSpeedPopupMenu = new PopupMenu(context, binding.playbackSpeed); |         playbackSpeedPopupMenu = new PopupMenu(context, binding.playbackSpeed); | ||||||
|         captionPopupMenu = new PopupMenu(themeWrapper, binding.captionTextView); |         captionPopupMenu = new PopupMenu(themeWrapper, binding.captionTextView); | ||||||
|  |  | ||||||
| @@ -190,6 +194,8 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa | |||||||
|  |  | ||||||
|     protected void initListeners() { |     protected void initListeners() { | ||||||
|         binding.qualityTextView.setOnClickListener(makeOnClickListener(this::onQualityClicked)); |         binding.qualityTextView.setOnClickListener(makeOnClickListener(this::onQualityClicked)); | ||||||
|  |         binding.languageTextView.setOnClickListener( | ||||||
|  |                 makeOnClickListener(this::onAudioLanguageClicked)); | ||||||
|         binding.playbackSpeed.setOnClickListener(makeOnClickListener(this::onPlaybackSpeedClicked)); |         binding.playbackSpeed.setOnClickListener(makeOnClickListener(this::onPlaybackSpeedClicked)); | ||||||
|  |  | ||||||
|         binding.playbackSeekBar.setOnSeekBarChangeListener(this); |         binding.playbackSeekBar.setOnSeekBarChangeListener(this); | ||||||
| @@ -266,6 +272,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa | |||||||
|  |  | ||||||
|     protected void deinitListeners() { |     protected void deinitListeners() { | ||||||
|         binding.qualityTextView.setOnClickListener(null); |         binding.qualityTextView.setOnClickListener(null); | ||||||
|  |         binding.languageTextView.setOnClickListener(null); | ||||||
|         binding.playbackSpeed.setOnClickListener(null); |         binding.playbackSpeed.setOnClickListener(null); | ||||||
|         binding.playbackSeekBar.setOnSeekBarChangeListener(null); |         binding.playbackSeekBar.setOnSeekBarChangeListener(null); | ||||||
|         binding.captionTextView.setOnClickListener(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.topControls.setPaddingRelative(controlsPad, playerTopPad, controlsPad, 0); | ||||||
|         binding.bottomControls.setPaddingRelative(controlsPad, 0, controlsPad, 0); |         binding.bottomControls.setPaddingRelative(controlsPad, 0, controlsPad, 0); | ||||||
|         binding.qualityTextView.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad); |         binding.qualityTextView.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad); | ||||||
|  |         binding.languageTextView.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad); | ||||||
|         binding.playbackSpeed.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad); |         binding.playbackSpeed.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad); | ||||||
|         binding.playbackSpeed.setMinimumWidth(buttonsMinWidth); |         binding.playbackSpeed.setMinimumWidth(buttonsMinWidth); | ||||||
|         binding.captionTextView.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad); |         binding.captionTextView.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad); | ||||||
| @@ -984,6 +992,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa | |||||||
|     private void updateStreamRelatedViews() { |     private void updateStreamRelatedViews() { | ||||||
|         player.getCurrentStreamInfo().ifPresent(info -> { |         player.getCurrentStreamInfo().ifPresent(info -> { | ||||||
|             binding.qualityTextView.setVisibility(View.GONE); |             binding.qualityTextView.setVisibility(View.GONE); | ||||||
|  |             binding.languageTextView.setVisibility(View.GONE); | ||||||
|             binding.playbackSpeed.setVisibility(View.GONE); |             binding.playbackSpeed.setVisibility(View.GONE); | ||||||
|  |  | ||||||
|             binding.playbackEndTime.setVisibility(View.GONE); |             binding.playbackEndTime.setVisibility(View.GONE); | ||||||
| @@ -1019,6 +1028,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa | |||||||
|                     } |                     } | ||||||
|  |  | ||||||
|                     buildQualityMenu(); |                     buildQualityMenu(); | ||||||
|  |                     buildLanguageMenu(); | ||||||
|  |  | ||||||
|                     binding.qualityTextView.setVisibility(View.VISIBLE); |                     binding.qualityTextView.setVisibility(View.VISIBLE); | ||||||
|                     binding.surfaceView.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())); |                 .ifPresent(s -> binding.qualityTextView.setText(s.getResolution())); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     private void buildLanguageMenu() { | ||||||
|  |         if (languagePopupMenu == null) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         languagePopupMenu.getMenu().removeGroup(POPUP_MENU_ID_LANGUAGE); | ||||||
|  |  | ||||||
|  |         final List<AudioStream> 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() { |     private void buildPlaybackSpeedMenu() { | ||||||
|         if (playbackSpeedPopupMenu == null) { |         if (playbackSpeedPopupMenu == null) { | ||||||
|             return; |             return; | ||||||
| @@ -1175,6 +1216,15 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa | |||||||
|                 .ifPresent(binding.qualityTextView::setText); |                 .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. |      * 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()); |             binding.qualityTextView.setText(menuItem.getTitle()); | ||||||
|             return true; |             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<AudioStream> 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) { |         } else if (menuItem.getGroupId() == POPUP_MENU_ID_PLAYBACK_SPEED) { | ||||||
|             final int speedIndex = menuItem.getItemId(); |             final int speedIndex = menuItem.getItemId(); | ||||||
|             final float speed = PLAYBACK_SPEEDS[speedIndex]; |             final float speed = PLAYBACK_SPEEDS[speedIndex]; | ||||||
|   | |||||||
| @@ -23,6 +23,7 @@ import java.util.Collections; | |||||||
| import java.util.Comparator; | import java.util.Comparator; | ||||||
| import java.util.HashMap; | import java.util.HashMap; | ||||||
| import java.util.List; | import java.util.List; | ||||||
|  | import java.util.Locale; | ||||||
| import java.util.Objects; | import java.util.Objects; | ||||||
| import java.util.Set; | import java.util.Set; | ||||||
| import java.util.function.Predicate; | import java.util.function.Predicate; | ||||||
| @@ -42,13 +43,14 @@ public final class ListHelper { | |||||||
|     // Use a Set for better performance |     // Use a Set for better performance | ||||||
|     private static final Set<String> HIGH_RESOLUTION_LIST = Set.of("1440p", "2160p"); |     private static final Set<String> HIGH_RESOLUTION_LIST = Set.of("1440p", "2160p"); | ||||||
|  |  | ||||||
|     private ListHelper() { } |     private ListHelper() { | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * @see #getDefaultResolutionIndex(String, String, MediaFormat, List) |  | ||||||
|      * @param context      Android app context |      * @param context      Android app context | ||||||
|      * @param videoStreams list of the video streams to check |      * @param videoStreams list of the video streams to check | ||||||
|      * @return index of the video stream with the default index |      * @return index of the video stream with the default index | ||||||
|  |      * @see #getDefaultResolutionIndex(String, String, MediaFormat, List) | ||||||
|      */ |      */ | ||||||
|     public static int getDefaultResolutionIndex(final Context context, |     public static int getDefaultResolutionIndex(final Context context, | ||||||
|                                                 final List<VideoStream> videoStreams) { |                                                 final List<VideoStream> videoStreams) { | ||||||
| @@ -58,11 +60,11 @@ public final class ListHelper { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * @see #getDefaultResolutionIndex(String, String, MediaFormat, List) |  | ||||||
|      * @param context           Android app context |      * @param context           Android app context | ||||||
|      * @param videoStreams      list of the video streams to check |      * @param videoStreams      list of the video streams to check | ||||||
|      * @param defaultResolution the default resolution to look for |      * @param defaultResolution the default resolution to look for | ||||||
|      * @return index of the video stream with the default index |      * @return index of the video stream with the default index | ||||||
|  |      * @see #getDefaultResolutionIndex(String, String, MediaFormat, List) | ||||||
|      */ |      */ | ||||||
|     public static int getResolutionIndex(final Context context, |     public static int getResolutionIndex(final Context context, | ||||||
|                                          final List<VideoStream> videoStreams, |                                          final List<VideoStream> videoStreams, | ||||||
| @@ -71,10 +73,10 @@ public final class ListHelper { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * @see #getDefaultResolutionIndex(String, String, MediaFormat, List) |  | ||||||
|      * @param context      Android app context |      * @param context      Android app context | ||||||
|      * @param videoStreams list of the video streams to check |      * @param videoStreams list of the video streams to check | ||||||
|      * @return index of the video stream with the default index |      * @return index of the video stream with the default index | ||||||
|  |      * @see #getDefaultResolutionIndex(String, String, MediaFormat, List) | ||||||
|      */ |      */ | ||||||
|     public static int getPopupDefaultResolutionIndex(final Context context, |     public static int getPopupDefaultResolutionIndex(final Context context, | ||||||
|                                                      final List<VideoStream> videoStreams) { |                                                      final List<VideoStream> videoStreams) { | ||||||
| @@ -84,11 +86,11 @@ public final class ListHelper { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * @see #getDefaultResolutionIndex(String, String, MediaFormat, List) |  | ||||||
|      * @param context           Android app context |      * @param context           Android app context | ||||||
|      * @param videoStreams      list of the video streams to check |      * @param videoStreams      list of the video streams to check | ||||||
|      * @param defaultResolution the default resolution to look for |      * @param defaultResolution the default resolution to look for | ||||||
|      * @return index of the video stream with the default index |      * @return index of the video stream with the default index | ||||||
|  |      * @see #getDefaultResolutionIndex(String, String, MediaFormat, List) | ||||||
|      */ |      */ | ||||||
|     public static int getPopupResolutionIndex(final Context context, |     public static int getPopupResolutionIndex(final Context context, | ||||||
|                                               final List<VideoStream> videoStreams, |                                               final List<VideoStream> videoStreams, | ||||||
| @@ -186,6 +188,80 @@ public final class ListHelper { | |||||||
|                 videoOnlyStreams, ascendingOrder, preferVideoOnlyStreams); |                 videoOnlyStreams, ascendingOrder, preferVideoOnlyStreams); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     public static List<AudioStream> getFilteredAudioStreams( | ||||||
|  |             @NonNull final Context context, | ||||||
|  |             @Nullable final List<AudioStream> audioStreams) { | ||||||
|  |         if (audioStreams == null) { | ||||||
|  |             return Collections.emptyList(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         final HashMap<String, AudioStream> collectedStreams = new HashMap<>(); | ||||||
|  |  | ||||||
|  |         final Comparator<AudioStream> 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 |     // Utils | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|   | |||||||
| @@ -157,6 +157,22 @@ | |||||||
|                             tools:text="The Video Artist  LONG very LONG very Long" /> |                             tools:text="The Video Artist  LONG very LONG very Long" /> | ||||||
|                     </LinearLayout> |                     </LinearLayout> | ||||||
|  |  | ||||||
|  |                     <org.schabi.newpipe.views.NewPipeTextView | ||||||
|  |                         android:id="@+id/languageTextView" | ||||||
|  |                         android:layout_width="wrap_content" | ||||||
|  |                         android:layout_height="35dp" | ||||||
|  |                         android:layout_marginEnd="8dp" | ||||||
|  |                         android:background="?attr/selectableItemBackground" | ||||||
|  |                         android:gravity="center" | ||||||
|  |                         android:minWidth="0dp" | ||||||
|  |                         android:padding="@dimen/player_main_buttons_padding" | ||||||
|  |                         android:textColor="@android:color/white" | ||||||
|  |                         android:textStyle="bold" | ||||||
|  |                         android:visibility="gone" | ||||||
|  |                         tools:ignore="HardcodedText,RtlHardcoded" | ||||||
|  |                         tools:visibility="visible" | ||||||
|  |                         tools:text="English (Original)" /> | ||||||
|  |  | ||||||
|                     <org.schabi.newpipe.views.NewPipeTextView |                     <org.schabi.newpipe.views.NewPipeTextView | ||||||
|                         android:id="@+id/qualityTextView" |                         android:id="@+id/qualityTextView" | ||||||
|                         android:layout_width="wrap_content" |                         android:layout_width="wrap_content" | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 ThetaDev
					ThetaDev