feat: add language selector to audio player

This commit is contained in:
ThetaDev 2023-03-19 01:15:36 +01:00
parent 77649d388c
commit 366c39d4c6
12 changed files with 241 additions and 98 deletions

View File

@ -13,6 +13,7 @@ import android.provider.Settings;
import android.util.Log; import android.util.Log;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.SubMenu;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.SeekBar; import android.widget.SeekBar;
@ -27,11 +28,13 @@ import com.google.android.exoplayer2.PlaybackParameters;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.ActivityPlayerQueueControlBinding; import org.schabi.newpipe.databinding.ActivityPlayerQueueControlBinding;
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.fragments.OnScrollBelowItemsListener; import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
import org.schabi.newpipe.local.dialog.PlaylistDialog; import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.player.event.PlayerEventListener; import org.schabi.newpipe.player.event.PlayerEventListener;
import org.schabi.newpipe.player.helper.PlaybackParameterDialog; import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
import org.schabi.newpipe.player.mediaitem.MediaItemTag;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueueAdapter; import org.schabi.newpipe.player.playqueue.PlayQueueAdapter;
import org.schabi.newpipe.player.playqueue.PlayQueueItem; import org.schabi.newpipe.player.playqueue.PlayQueueItem;
@ -44,6 +47,10 @@ import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.ServiceHelper; import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
public final class PlayQueueActivity extends AppCompatActivity public final class PlayQueueActivity extends AppCompatActivity
implements PlayerEventListener, SeekBar.OnSeekBarChangeListener, implements PlayerEventListener, SeekBar.OnSeekBarChangeListener,
View.OnClickListener, PlaybackParameterDialog.Callback { View.OnClickListener, PlaybackParameterDialog.Callback {
@ -52,6 +59,8 @@ public final class PlayQueueActivity extends AppCompatActivity
private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80; private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80;
private static final int MENU_ID_AUDIO_TRACK = 71;
private Player player; private Player player;
private boolean serviceBound; private boolean serviceBound;
@ -97,6 +106,7 @@ public final class PlayQueueActivity extends AppCompatActivity
this.menu = m; this.menu = m;
getMenuInflater().inflate(R.menu.menu_play_queue, m); getMenuInflater().inflate(R.menu.menu_play_queue, m);
getMenuInflater().inflate(R.menu.menu_play_queue_bg, m); getMenuInflater().inflate(R.menu.menu_play_queue_bg, m);
buildAudioTrackMenu();
onMaybeMuteChanged(); onMaybeMuteChanged();
// to avoid null reference // to avoid null reference
if (player != null) { if (player != null) {
@ -153,6 +163,12 @@ public final class PlayQueueActivity extends AppCompatActivity
NavigationHelper.playOnBackgroundPlayer(this, player.getPlayQueue(), true); NavigationHelper.playOnBackgroundPlayer(this, player.getPlayQueue(), true);
return true; return true;
} }
if (item.getGroupId() == MENU_ID_AUDIO_TRACK) {
onAudioTrackClick(item.getItemId());
return true;
}
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
@ -591,4 +607,77 @@ public final class PlayQueueActivity extends AppCompatActivity
item.setIcon(player.isMuted() ? R.drawable.ic_volume_off : R.drawable.ic_volume_up); item.setIcon(player.isMuted() ? R.drawable.ic_volume_off : R.drawable.ic_volume_up);
} }
} }
@Override
public void onAudioTrackUpdate() {
buildAudioTrackMenu();
}
private void buildAudioTrackMenu() {
if (menu == null) {
return;
}
final MenuItem audioTrackSelector = menu.findItem(R.id.action_audio_track);
final List<AudioStream> availableStreams =
Optional.ofNullable(player.getCurrentMetadata())
.flatMap(MediaItemTag::getMaybeAudioTrack)
.map(MediaItemTag.AudioTrack::getAudioStreams)
.orElse(null);
if (availableStreams == null || availableStreams.size() < 2) {
audioTrackSelector.setVisible(false);
} else {
final SubMenu audioTrackMenu = audioTrackSelector.getSubMenu();
audioTrackMenu.clear();
for (int i = 0; i < availableStreams.size(); i++) {
final AudioStream audioStream = availableStreams.get(i);
if (audioStream.getAudioTrackName() == null) {
continue;
}
audioTrackMenu.add(MENU_ID_AUDIO_TRACK, i, Menu.NONE,
audioStream.getAudioTrackName());
}
player.getSelectedAudioStream().ifPresent(s -> {
final String trackName = Objects.toString(s.getAudioTrackName(), "");
audioTrackSelector.setTitle(getString(R.string.play_queue_audio_track) + trackName);
final String shortName = s.getAudioLocale() != null
? s.getAudioLocale().getLanguage() : trackName;
audioTrackSelector.setTitleCondensed(
shortName.substring(0, Math.min(shortName.length(), 2)));
audioTrackSelector.setVisible(true);
});
}
}
/**
* Called when an item from the audio track selector is selected.
*
* @param itemId index of the selected item
*/
private void onAudioTrackClick(final int itemId) {
@Nullable final MediaItemTag currentMetadata = player.getCurrentMetadata();
if (currentMetadata == null || currentMetadata.getMaybeAudioTrack().isEmpty()) {
return;
}
final MediaItemTag.AudioTrack audioTrack =
currentMetadata.getMaybeAudioTrack().get();
final List<AudioStream> availableStreams = audioTrack.getAudioStreams();
final int selectedStreamIndex = audioTrack.getSelectedAudioStreamIndex();
if (selectedStreamIndex == itemId || availableStreams.size() <= itemId) {
return;
}
player.saveStreamProgressState();
final String newAudioTrack = availableStreams.get(itemId).getAudioTrackId();
player.setRecovery();
player.setAudioTrack(newAudioTrack);
player.reloadPlayQueueManager();
}
} }

View File

@ -1243,6 +1243,9 @@ public final class Player implements PlaybackListener, Listener {
} }
final StreamInfo previousInfo = Optional.ofNullable(currentMetadata) final StreamInfo previousInfo = Optional.ofNullable(currentMetadata)
.flatMap(MediaItemTag::getMaybeStreamInfo).orElse(null); .flatMap(MediaItemTag::getMaybeStreamInfo).orElse(null);
final MediaItemTag.AudioTrack previousAudioTrack =
Optional.ofNullable(currentMetadata)
.flatMap(MediaItemTag::getMaybeAudioTrack).orElse(null);
currentMetadata = tag; currentMetadata = tag;
if (!currentMetadata.getErrors().isEmpty()) { if (!currentMetadata.getErrors().isEmpty()) {
@ -1263,6 +1266,12 @@ public final class Player implements PlaybackListener, Listener {
if (previousInfo == null || !previousInfo.getUrl().equals(info.getUrl())) { if (previousInfo == null || !previousInfo.getUrl().equals(info.getUrl())) {
// only update with the new stream info if it has actually changed // only update with the new stream info if it has actually changed
updateMetadataWith(info); updateMetadataWith(info);
} else if (previousAudioTrack == null
|| tag.getMaybeAudioTrack()
.map(t -> t.getSelectedAudioStreamIndex()
!= previousAudioTrack.getSelectedAudioStreamIndex())
.orElse(false)) {
notifyAudioTrackUpdateToListeners();
} }
}); });
}); });
@ -1759,6 +1768,7 @@ public final class Player implements PlaybackListener, Listener {
registerStreamViewed(); registerStreamViewed();
notifyMetadataUpdateToListeners(); notifyMetadataUpdateToListeners();
notifyAudioTrackUpdateToListeners();
UIs.call(playerUi -> playerUi.onMetadataChanged(info)); UIs.call(playerUi -> playerUi.onMetadataChanged(info));
} }
@ -1890,8 +1900,8 @@ public final class Player implements PlaybackListener, Listener {
public Optional<AudioStream> getSelectedAudioStream() { public Optional<AudioStream> getSelectedAudioStream() {
return Optional.ofNullable(currentMetadata) return Optional.ofNullable(currentMetadata)
.flatMap(MediaItemTag::getMaybeAudioLanguage) .flatMap(MediaItemTag::getMaybeAudioTrack)
.map(MediaItemTag.AudioLanguage::getSelectedAudioStream); .map(MediaItemTag.AudioTrack::getSelectedAudioStream);
} }
//endregion //endregion
@ -2024,6 +2034,15 @@ public final class Player implements PlaybackListener, Listener {
} }
} }
private void notifyAudioTrackUpdateToListeners() {
if (fragmentListener != null) {
fragmentListener.onAudioTrackUpdate();
}
if (activityListener != null) {
activityListener.onAudioTrackUpdate();
}
}
public void useVideoSource(final boolean videoEnabled) { public void useVideoSource(final boolean videoEnabled) {
if (playQueue == null || isAudioOnly == !videoEnabled || audioPlayerSelected()) { if (playQueue == null || isAudioOnly == !videoEnabled || audioPlayerSelected()) {
return; return;
@ -2185,8 +2204,9 @@ public final class Player implements PlaybackListener, Listener {
videoResolver.setPlaybackQuality(quality); videoResolver.setPlaybackQuality(quality);
} }
public void setAudioLanguage(@Nullable final String language) { public void setAudioTrack(@Nullable final String audioTrackId) {
videoResolver.setAudioLanguage(language); videoResolver.setAudioTrack(audioTrackId);
audioResolver.setAudioTrack(audioTrackId);
} }

View File

@ -11,5 +11,6 @@ public interface PlayerEventListener {
PlaybackParameters parameters); PlaybackParameters parameters);
void onProgressUpdate(int currentProgress, int duration, int bufferPercent); void onProgressUpdate(int currentProgress, int duration, int bufferPercent);
void onMetadataUpdate(StreamInfo info, PlayQueue queue); void onMetadataUpdate(StreamInfo info, PlayQueue queue);
default void onAudioTrackUpdate() { }
void onServiceStopped(); void onServiceStopped();
} }

View File

@ -57,7 +57,7 @@ public interface MediaItemTag {
} }
@NonNull @NonNull
default Optional<AudioLanguage> getMaybeAudioLanguage() { default Optional<AudioTrack> getMaybeAudioTrack() {
return Optional.empty(); return Optional.empty();
} }
@ -135,20 +135,20 @@ public interface MediaItemTag {
} }
} }
final class AudioLanguage { final class AudioTrack {
@NonNull @NonNull
private final List<AudioStream> audioStreams; private final List<AudioStream> audioStreams;
private final int selectedAudioStreamIndex; private final int selectedAudioStreamIndex;
private AudioLanguage(@NonNull final List<AudioStream> audioStreams, private AudioTrack(@NonNull final List<AudioStream> audioStreams,
final int selectedAudioStreamIndex) { final int selectedAudioStreamIndex) {
this.audioStreams = audioStreams; this.audioStreams = audioStreams;
this.selectedAudioStreamIndex = selectedAudioStreamIndex; this.selectedAudioStreamIndex = selectedAudioStreamIndex;
} }
static AudioLanguage of(@NonNull final List<AudioStream> audioStreams, static AudioTrack of(@NonNull final List<AudioStream> audioStreams,
final int selectedAudioStreamIndex) { final int selectedAudioStreamIndex) {
return new AudioLanguage(audioStreams, selectedAudioStreamIndex); return new AudioTrack(audioStreams, selectedAudioStreamIndex);
} }
@NonNull @NonNull

View File

@ -26,17 +26,17 @@ 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; private final MediaItemTag.AudioTrack audioTrack;
@Nullable @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 MediaItemTag.AudioTrack audioTrack,
@Nullable final Object extras) { @Nullable final Object extras) {
this.streamInfo = streamInfo; this.streamInfo = streamInfo;
this.quality = quality; this.quality = quality;
this.audioLanguage = audioLanguage; this.audioTrack = audioTrack;
this.extras = extras; this.extras = extras;
} }
@ -46,9 +46,17 @@ public final class StreamInfoTag implements MediaItemTag {
@NonNull final List<AudioStream> audioStreams, @NonNull final List<AudioStream> audioStreams,
final int selectedAudioStreamIndex) { final int selectedAudioStreamIndex) {
final Quality quality = Quality.of(sortedVideoStreams, selectedVideoStreamIndex); final Quality quality = Quality.of(sortedVideoStreams, selectedVideoStreamIndex);
final AudioLanguage audioLanguage = final AudioTrack audioTrack =
AudioLanguage.of(audioStreams, selectedAudioStreamIndex); AudioTrack.of(audioStreams, selectedAudioStreamIndex);
return new StreamInfoTag(streamInfo, quality, audioLanguage, null); return new StreamInfoTag(streamInfo, quality, audioTrack, null);
}
public static StreamInfoTag of(@NonNull final StreamInfo streamInfo,
@NonNull final List<AudioStream> audioStreams,
final int selectedAudioStreamIndex) {
final AudioTrack audioTrack =
AudioTrack.of(audioStreams, selectedAudioStreamIndex);
return new StreamInfoTag(streamInfo, null, audioTrack, null);
} }
public static StreamInfoTag of(@NonNull final StreamInfo streamInfo) { public static StreamInfoTag of(@NonNull final StreamInfo streamInfo) {
@ -114,8 +122,8 @@ public final class StreamInfoTag implements MediaItemTag {
@NonNull @NonNull
@Override @Override
public Optional<AudioLanguage> getMaybeAudioLanguage() { public Optional<AudioTrack> getMaybeAudioTrack() {
return Optional.ofNullable(audioLanguage); return Optional.ofNullable(audioTrack);
} }
@Override @Override
@ -125,6 +133,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, audioLanguage, extra); return new StreamInfoTag(streamInfo, quality, audioTrack, extra);
} }
} }

View File

@ -1,5 +1,6 @@
package org.schabi.newpipe.player.resolver; package org.schabi.newpipe.player.resolver;
import static org.schabi.newpipe.util.ListHelper.getFilteredAudioStreams;
import static org.schabi.newpipe.util.ListHelper.getNonTorrentStreams; import static org.schabi.newpipe.util.ListHelper.getNonTorrentStreams;
import android.content.Context; import android.content.Context;
@ -28,6 +29,8 @@ public class AudioPlaybackResolver implements PlaybackResolver {
private final Context context; private final Context context;
@NonNull @NonNull
private final PlayerDataSource dataSource; private final PlayerDataSource dataSource;
@Nullable
private String audioTrack;
public AudioPlaybackResolver(@NonNull final Context context, public AudioPlaybackResolver(@NonNull final Context context,
@NonNull final PlayerDataSource dataSource) { @NonNull final PlayerDataSource dataSource) {
@ -43,12 +46,36 @@ public class AudioPlaybackResolver implements PlaybackResolver {
return liveSource; return liveSource;
} }
final Stream stream = getAudioSource(info); final List<AudioStream> audioStreams =
if (stream == null) { getFilteredAudioStreams(context, info.getAudioStreams());
final Stream stream;
final MediaItemTag tag;
if (!audioStreams.isEmpty()) {
int audioIndex = 0;
if (audioTrack != null) {
for (int i = 0; i < audioStreams.size(); i++) {
final AudioStream audioStream = audioStreams.get(i);
if (audioStream.getAudioTrackId() != null
&& audioStream.getAudioTrackId().equals(audioTrack)) {
audioIndex = i;
break;
}
}
}
stream = getStreamForIndex(audioIndex, audioStreams);
tag = StreamInfoTag.of(info, audioStreams, audioIndex);
} else {
final List<VideoStream> videoStreams = getNonTorrentStreams(info.getVideoStreams());
if (!videoStreams.isEmpty()) {
final int index = ListHelper.getDefaultResolutionIndex(context, videoStreams);
stream = getStreamForIndex(index, videoStreams);
tag = StreamInfoTag.of(info);
} else {
return null; return null;
} }
}
final MediaItemTag tag = StreamInfoTag.of(info);
try { try {
return PlaybackResolver.buildMediaSource( return PlaybackResolver.buildMediaSource(
@ -59,29 +86,6 @@ public class AudioPlaybackResolver implements PlaybackResolver {
} }
} }
/**
* Get a stream to be played as audio. If a service has no separate {@link AudioStream}s we
* use a video stream as audio source to support audio background playback.
*
* @param info of the stream
* @return the audio source to use or null if none could be found
*/
@Nullable
private Stream getAudioSource(@NonNull final StreamInfo info) {
final List<AudioStream> audioStreams = getNonTorrentStreams(info.getAudioStreams());
if (!audioStreams.isEmpty()) {
final int index = ListHelper.getDefaultAudioFormat(context, audioStreams);
return getStreamForIndex(index, audioStreams);
} else {
final List<VideoStream> videoStreams = getNonTorrentStreams(info.getVideoStreams());
if (!videoStreams.isEmpty()) {
final int index = ListHelper.getDefaultResolutionIndex(context, videoStreams);
return getStreamForIndex(index, videoStreams);
}
}
return null;
}
@Nullable @Nullable
Stream getStreamForIndex(final int index, @NonNull final List<? extends Stream> streams) { Stream getStreamForIndex(final int index, @NonNull final List<? extends Stream> streams) {
if (index >= 0 && index < streams.size()) { if (index >= 0 && index < streams.size()) {
@ -89,4 +93,13 @@ public class AudioPlaybackResolver implements PlaybackResolver {
} }
return null; return null;
} }
@Nullable
public String getAudioTrack() {
return audioTrack;
}
public void setAudioTrack(@Nullable final String audioLanguage) {
this.audioTrack = audioLanguage;
}
} }

View File

@ -46,7 +46,7 @@ public class VideoPlaybackResolver implements PlaybackResolver {
@Nullable @Nullable
private String playbackQuality; private String playbackQuality;
@Nullable @Nullable
private String audioLanguage; private String audioTrack;
public enum SourceType { public enum SourceType {
LIVE_STREAM, LIVE_STREAM,
@ -91,11 +91,11 @@ public class VideoPlaybackResolver implements PlaybackResolver {
} }
int audioIndex = 0; int audioIndex = 0;
if (audioLanguage != null) { if (audioTrack != null) {
for (int i = 0; i < audioStreamsList.size(); i++) { for (int i = 0; i < audioStreamsList.size(); i++) {
final AudioStream stream = audioStreamsList.get(i); final AudioStream stream = audioStreamsList.get(i);
if (stream.getAudioTrackId() != null if (stream.getAudioTrackId() != null
&& stream.getAudioTrackId().equals(audioLanguage)) { && stream.getAudioTrackId().equals(audioTrack)) {
audioIndex = i; audioIndex = i;
break; break;
} }
@ -107,8 +107,8 @@ public class VideoPlaybackResolver implements PlaybackResolver {
@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() @Nullable final AudioStream audio = tag.getMaybeAudioTrack()
.map(MediaItemTag.AudioLanguage::getSelectedAudioStream) .map(MediaItemTag.AudioTrack::getSelectedAudioStream)
.orElse(null); .orElse(null);
if (video != null) { if (video != null) {
@ -124,7 +124,7 @@ public class VideoPlaybackResolver implements PlaybackResolver {
// 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() || audioLanguage != null)) { if (audio != null && (video == null || video.isVideoOnly() || audioTrack != 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);
@ -198,12 +198,12 @@ public class VideoPlaybackResolver implements PlaybackResolver {
} }
@Nullable @Nullable
public String getAudioLanguage() { public String getAudioTrack() {
return audioLanguage; return audioTrack;
} }
public void setAudioLanguage(@Nullable final String audioLanguage) { public void setAudioTrack(@Nullable final String audioLanguage) {
this.audioLanguage = audioLanguage; this.audioTrack = audioLanguage;
} }
public interface QualityResolver { public interface QualityResolver {
@ -211,10 +211,4 @@ public class VideoPlaybackResolver implements PlaybackResolver {
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);
}
} }

View File

@ -118,13 +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_AUDIO_TRACK = 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; private PopupMenu audioTrackPopupMenu;
protected PopupMenu playbackSpeedPopupMenu; protected PopupMenu playbackSpeedPopupMenu;
private PopupMenu captionPopupMenu; private PopupMenu captionPopupMenu;
@ -176,7 +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); audioTrackPopupMenu = new PopupMenu(themeWrapper, binding.audioTrackTextView);
playbackSpeedPopupMenu = new PopupMenu(context, binding.playbackSpeed); playbackSpeedPopupMenu = new PopupMenu(context, binding.playbackSpeed);
captionPopupMenu = new PopupMenu(themeWrapper, binding.captionTextView); captionPopupMenu = new PopupMenu(themeWrapper, binding.captionTextView);
@ -194,8 +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( binding.audioTrackTextView.setOnClickListener(
makeOnClickListener(this::onAudioLanguageClicked)); makeOnClickListener(this::onAudioTracksClicked));
binding.playbackSpeed.setOnClickListener(makeOnClickListener(this::onPlaybackSpeedClicked)); binding.playbackSpeed.setOnClickListener(makeOnClickListener(this::onPlaybackSpeedClicked));
binding.playbackSeekBar.setOnSeekBarChangeListener(this); binding.playbackSeekBar.setOnSeekBarChangeListener(this);
@ -272,7 +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.audioTrackTextView.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);
@ -426,7 +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.audioTrackTextView.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);
@ -992,7 +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.audioTrackTextView.setVisibility(View.GONE);
binding.playbackSpeed.setVisibility(View.GONE); binding.playbackSpeed.setVisibility(View.GONE);
binding.playbackEndTime.setVisibility(View.GONE); binding.playbackEndTime.setVisibility(View.GONE);
@ -1028,7 +1028,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
} }
buildQualityMenu(); buildQualityMenu();
buildLanguageMenu(); buildAudioTrackMenu();
binding.qualityTextView.setVisibility(View.VISIBLE); binding.qualityTextView.setVisibility(View.VISIBLE);
binding.surfaceView.setVisibility(View.VISIBLE); binding.surfaceView.setVisibility(View.VISIBLE);
@ -1077,15 +1077,15 @@ 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() { private void buildAudioTrackMenu() {
if (languagePopupMenu == null) { if (audioTrackPopupMenu == null) {
return; return;
} }
languagePopupMenu.getMenu().removeGroup(POPUP_MENU_ID_LANGUAGE); audioTrackPopupMenu.getMenu().removeGroup(POPUP_MENU_ID_AUDIO_TRACK);
final List<AudioStream> availableStreams = Optional.ofNullable(player.getCurrentMetadata()) final List<AudioStream> availableStreams = Optional.ofNullable(player.getCurrentMetadata())
.flatMap(MediaItemTag::getMaybeAudioLanguage) .flatMap(MediaItemTag::getMaybeAudioTrack)
.map(MediaItemTag.AudioLanguage::getAudioStreams) .map(MediaItemTag.AudioTrack::getAudioStreams)
.orElse(null); .orElse(null);
if (availableStreams == null || availableStreams.size() < 2) { if (availableStreams == null || availableStreams.size() < 2) {
return; return;
@ -1096,15 +1096,15 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
if (audioStream.getAudioTrackName() == null) { if (audioStream.getAudioTrackName() == null) {
continue; continue;
} }
languagePopupMenu.getMenu().add(POPUP_MENU_ID_LANGUAGE, i, Menu.NONE, audioTrackPopupMenu.getMenu().add(POPUP_MENU_ID_AUDIO_TRACK, i, Menu.NONE,
audioStream.getAudioTrackName()); audioStream.getAudioTrackName());
} }
player.getSelectedAudioStream() player.getSelectedAudioStream()
.ifPresent(s -> binding.languageTextView.setText(s.getAudioTrackName())); .ifPresent(s -> binding.audioTrackTextView.setText(s.getAudioTrackName()));
binding.languageTextView.setVisibility(View.VISIBLE); binding.audioTrackTextView.setVisibility(View.VISIBLE);
languagePopupMenu.setOnMenuItemClickListener(this); audioTrackPopupMenu.setOnMenuItemClickListener(this);
languagePopupMenu.setOnDismissListener(this); audioTrackPopupMenu.setOnDismissListener(this);
} }
private void buildPlaybackSpeedMenu() { private void buildPlaybackSpeedMenu() {
@ -1215,13 +1215,13 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
.ifPresent(binding.qualityTextView::setText); .ifPresent(binding.qualityTextView::setText);
} }
private void onAudioLanguageClicked() { private void onAudioTracksClicked() {
languagePopupMenu.show(); audioTrackPopupMenu.show();
isSomePopupMenuVisible = true; isSomePopupMenuVisible = true;
player.getSelectedAudioStream() player.getSelectedAudioStream()
.map(AudioStream::getAudioTrackName) .map(AudioStream::getAudioTrackName)
.ifPresent(binding.languageTextView::setText); .ifPresent(binding.audioTrackTextView::setText);
} }
/** /**
@ -1238,8 +1238,8 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
if (menuItem.getGroupId() == POPUP_MENU_ID_QUALITY) { if (menuItem.getGroupId() == POPUP_MENU_ID_QUALITY) {
onQualityItemClick(menuItem); onQualityItemClick(menuItem);
return true; return true;
} else if (menuItem.getGroupId() == POPUP_MENU_ID_LANGUAGE) { } else if (menuItem.getGroupId() == POPUP_MENU_ID_AUDIO_TRACK) {
onLanguageItemClick(menuItem); onAudioTrackItemClick(menuItem);
return true; 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();
@ -1275,28 +1275,28 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
binding.qualityTextView.setText(menuItem.getTitle()); binding.qualityTextView.setText(menuItem.getTitle());
} }
private void onLanguageItemClick(@NonNull final MenuItem menuItem) { private void onAudioTrackItemClick(@NonNull final MenuItem menuItem) {
final int menuItemIndex = menuItem.getItemId(); final int menuItemIndex = menuItem.getItemId();
@Nullable final MediaItemTag currentMetadata = player.getCurrentMetadata(); @Nullable final MediaItemTag currentMetadata = player.getCurrentMetadata();
if (currentMetadata == null || currentMetadata.getMaybeAudioLanguage().isEmpty()) { if (currentMetadata == null || currentMetadata.getMaybeAudioTrack().isEmpty()) {
return; return;
} }
final MediaItemTag.AudioLanguage language = final MediaItemTag.AudioTrack audioTrack =
currentMetadata.getMaybeAudioLanguage().get(); currentMetadata.getMaybeAudioTrack().get();
final List<AudioStream> availableStreams = language.getAudioStreams(); final List<AudioStream> availableStreams = audioTrack.getAudioStreams();
final int selectedStreamIndex = language.getSelectedAudioStreamIndex(); final int selectedStreamIndex = audioTrack.getSelectedAudioStreamIndex();
if (selectedStreamIndex == menuItemIndex || availableStreams.size() <= menuItemIndex) { if (selectedStreamIndex == menuItemIndex || availableStreams.size() <= menuItemIndex) {
return; return;
} }
player.saveStreamProgressState(); player.saveStreamProgressState();
final String newLanguage = availableStreams.get(menuItemIndex).getAudioTrackId(); final String newAudioTrack = availableStreams.get(menuItemIndex).getAudioTrackId();
player.setRecovery(); player.setRecovery();
player.setAudioLanguage(newLanguage); player.setAudioTrack(newAudioTrack);
player.reloadPlayQueueManager(); player.reloadPlayQueueManager();
binding.languageTextView.setText(menuItem.getTitle()); binding.audioTrackTextView.setText(menuItem.getTitle());
} }
/** /**

View File

@ -188,6 +188,14 @@ public final class ListHelper {
videoOnlyStreams, ascendingOrder, preferVideoOnlyStreams); videoOnlyStreams, ascendingOrder, preferVideoOnlyStreams);
} }
/**
* Filter the list of audio streams and return a list with the preferred stream for
* each audio track. Streams are sorted with the preferred language in the first position.
*
* @param context the context to search for the track to give preference
* @param audioStreams the list of audio streams
* @return the sorted, filtered list
*/
public static List<AudioStream> getFilteredAudioStreams( public static List<AudioStream> getFilteredAudioStreams(
@NonNull final Context context, @NonNull final Context context,
@Nullable final List<AudioStream> audioStreams) { @Nullable final List<AudioStream> audioStreams) {

View File

@ -158,7 +158,7 @@
</LinearLayout> </LinearLayout>
<org.schabi.newpipe.views.NewPipeTextView <org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/languageTextView" android:id="@+id/audioTrackTextView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="35dp" android:layout_height="35dp"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"

View File

@ -18,6 +18,14 @@
android:visible="true" android:visible="true"
app:showAsAction="ifRoom" /> app:showAsAction="ifRoom" />
<item
android:id="@+id/action_audio_track"
android:tooltipText="@string/audio_track"
android:visible="false"
app:showAsAction="ifRoom">
<menu />
</item>
<item <item
android:id="@+id/action_mute" android:id="@+id/action_mute"
android:icon="@drawable/ic_volume_off" android:icon="@drawable/ic_volume_off"

View File

@ -413,6 +413,8 @@
<string name="play_queue_remove">Remove</string> <string name="play_queue_remove">Remove</string>
<string name="play_queue_stream_detail">Details</string> <string name="play_queue_stream_detail">Details</string>
<string name="play_queue_audio_settings">Audio Settings</string> <string name="play_queue_audio_settings">Audio Settings</string>
<string name="play_queue_audio_track">Audio: </string>
<string name="audio_track">Audio track</string>
<string name="hold_to_append">Hold to enqueue</string> <string name="hold_to_append">Hold to enqueue</string>
<string name="show_channel_details">Show channel details</string> <string name="show_channel_details">Show channel details</string>
<string name="enqueue_stream">Enqueue</string> <string name="enqueue_stream">Enqueue</string>