feat: add audio language selector
This commit is contained in:
parent
d33229a3b8
commit
de7872d8f2
9 changed files with 295 additions and 26 deletions
|
@ -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;
|
||||||
|
|
||||||
|
@ -146,7 +149,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
||||||
//region Constructor, setup, destroy
|
//region Constructor, setup, destroy
|
||||||
|
|
||||||
protected VideoPlayerUi(@NonNull final Player player,
|
protected VideoPlayerUi(@NonNull final Player player,
|
||||||
@NonNull final PlayerBinding playerBinding) {
|
@NonNull final PlayerBinding playerBinding) {
|
||||||
super(player);
|
super(player);
|
||||||
binding = playerBinding;
|
binding = playerBinding;
|
||||||
setupFromView();
|
setupFromView();
|
||||||
|
@ -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
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
@ -300,8 +376,8 @@ public final class ListHelper {
|
||||||
// Filter out higher resolutions (or not if high resolutions should always be shown)
|
// Filter out higher resolutions (or not if high resolutions should always be shown)
|
||||||
.filter(stream -> showHigherResolutions
|
.filter(stream -> showHigherResolutions
|
||||||
|| !HIGH_RESOLUTION_LIST.contains(stream.getResolution()
|
|| !HIGH_RESOLUTION_LIST.contains(stream.getResolution()
|
||||||
// Replace any frame rate with nothing
|
// Replace any frame rate with nothing
|
||||||
.replaceAll("p\\d+$", "p")))
|
.replaceAll("p\\d+$", "p")))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
final HashMap<String, VideoStream> hashMap = new HashMap<>();
|
final HashMap<String, VideoStream> hashMap = new HashMap<>();
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in a new issue