1
0
mirror of https://github.com/TeamNewPipe/NewPipe.git synced 2024-11-25 12:32:31 +01:00

Merge pull request #9937 from Theta-Dev/alang-selector

Add support for multiple audio tracks
This commit is contained in:
Stypox 2023-05-02 10:07:21 +02:00 committed by GitHub
commit 2315b082ff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1190 additions and 259 deletions

View File

@ -68,6 +68,8 @@ import org.schabi.newpipe.util.SecondaryStreamHelper;
import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener; import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener;
import org.schabi.newpipe.util.StreamItemAdapter; import org.schabi.newpipe.util.StreamItemAdapter;
import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper; import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper;
import org.schabi.newpipe.util.AudioTrackAdapter;
import org.schabi.newpipe.util.AudioTrackAdapter.AudioTracksWrapper;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
import java.io.File; import java.io.File;
@ -95,12 +97,14 @@ public class DownloadDialog extends DialogFragment
@State @State
StreamInfo currentInfo; StreamInfo currentInfo;
@State @State
StreamSizeWrapper<AudioStream> wrappedAudioStreams;
@State
StreamSizeWrapper<VideoStream> wrappedVideoStreams; StreamSizeWrapper<VideoStream> wrappedVideoStreams;
@State @State
StreamSizeWrapper<SubtitlesStream> wrappedSubtitleStreams; StreamSizeWrapper<SubtitlesStream> wrappedSubtitleStreams;
@State @State
AudioTracksWrapper wrappedAudioTracks;
@State
int selectedAudioTrackIndex;
@State
int selectedVideoIndex; // set in the constructor int selectedVideoIndex; // set in the constructor
@State @State
int selectedAudioIndex = 0; // default to the first item int selectedAudioIndex = 0; // default to the first item
@ -117,6 +121,7 @@ public class DownloadDialog extends DialogFragment
private Context context; private Context context;
private boolean askForSavePath; private boolean askForSavePath;
private AudioTrackAdapter audioTrackAdapter;
private StreamItemAdapter<AudioStream, Stream> audioStreamsAdapter; private StreamItemAdapter<AudioStream, Stream> audioStreamsAdapter;
private StreamItemAdapter<VideoStream, AudioStream> videoStreamsAdapter; private StreamItemAdapter<VideoStream, AudioStream> videoStreamsAdapter;
private StreamItemAdapter<SubtitlesStream, Stream> subtitleStreamsAdapter; private StreamItemAdapter<SubtitlesStream, Stream> subtitleStreamsAdapter;
@ -163,18 +168,26 @@ public class DownloadDialog extends DialogFragment
public DownloadDialog(@NonNull final Context context, @NonNull final StreamInfo info) { public DownloadDialog(@NonNull final Context context, @NonNull final StreamInfo info) {
this.currentInfo = info; this.currentInfo = info;
final List<AudioStream> audioStreams =
getStreamsOfSpecifiedDelivery(info.getAudioStreams(), PROGRESSIVE_HTTP);
final List<List<AudioStream>> groupedAudioStreams =
ListHelper.getGroupedAudioStreams(context, audioStreams);
this.wrappedAudioTracks = new AudioTracksWrapper(groupedAudioStreams, context);
this.selectedAudioTrackIndex =
ListHelper.getDefaultAudioTrackGroup(context, groupedAudioStreams);
// TODO: Adapt this code when the downloader support other types of stream deliveries // TODO: Adapt this code when the downloader support other types of stream deliveries
final List<VideoStream> videoStreams = ListHelper.getSortedStreamVideosList( final List<VideoStream> videoStreams = ListHelper.getSortedStreamVideosList(
context, context,
getStreamsOfSpecifiedDelivery(info.getVideoStreams(), PROGRESSIVE_HTTP), getStreamsOfSpecifiedDelivery(info.getVideoStreams(), PROGRESSIVE_HTTP),
getStreamsOfSpecifiedDelivery(info.getVideoOnlyStreams(), PROGRESSIVE_HTTP), getStreamsOfSpecifiedDelivery(info.getVideoOnlyStreams(), PROGRESSIVE_HTTP),
false, false,
false // If there are multiple languages available, prefer streams without audio
// to allow language selection
wrappedAudioTracks.size() > 1
); );
this.wrappedVideoStreams = new StreamSizeWrapper<>(videoStreams, context); this.wrappedVideoStreams = new StreamSizeWrapper<>(videoStreams, context);
this.wrappedAudioStreams = new StreamSizeWrapper<>(
getStreamsOfSpecifiedDelivery(info.getAudioStreams(), PROGRESSIVE_HTTP), context);
this.wrappedSubtitleStreams = new StreamSizeWrapper<>( this.wrappedSubtitleStreams = new StreamSizeWrapper<>(
getStreamsOfSpecifiedDelivery(info.getSubtitles(), PROGRESSIVE_HTTP), context); getStreamsOfSpecifiedDelivery(info.getSubtitles(), PROGRESSIVE_HTTP), context);
@ -212,33 +225,9 @@ public class DownloadDialog extends DialogFragment
setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context)); setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context));
Icepick.restoreInstanceState(this, savedInstanceState); Icepick.restoreInstanceState(this, savedInstanceState);
final var secondaryStreams = new SparseArrayCompat<SecondaryStreamHelper<AudioStream>>(4); this.audioTrackAdapter = new AudioTrackAdapter(wrappedAudioTracks);
final List<VideoStream> videoStreams = wrappedVideoStreams.getStreamsList();
for (int i = 0; i < videoStreams.size(); i++) {
if (!videoStreams.get(i).isVideoOnly()) {
continue;
}
final AudioStream audioStream = SecondaryStreamHelper
.getAudioStreamFor(wrappedAudioStreams.getStreamsList(), videoStreams.get(i));
if (audioStream != null) {
secondaryStreams.append(i, new SecondaryStreamHelper<>(wrappedAudioStreams,
audioStream));
} else if (DEBUG) {
final MediaFormat mediaFormat = videoStreams.get(i).getFormat();
if (mediaFormat != null) {
Log.w(TAG, "No audio stream candidates for video format "
+ mediaFormat.name());
} else {
Log.w(TAG, "No audio stream candidates for unknown video format");
}
}
}
this.videoStreamsAdapter = new StreamItemAdapter<>(wrappedVideoStreams, secondaryStreams);
this.audioStreamsAdapter = new StreamItemAdapter<>(wrappedAudioStreams);
this.subtitleStreamsAdapter = new StreamItemAdapter<>(wrappedSubtitleStreams); this.subtitleStreamsAdapter = new StreamItemAdapter<>(wrappedSubtitleStreams);
updateSecondaryStreams();
final Intent intent = new Intent(context, DownloadManagerService.class); final Intent intent = new Intent(context, DownloadManagerService.class);
context.startService(intent); context.startService(intent);
@ -265,6 +254,39 @@ public class DownloadDialog extends DialogFragment
}, Context.BIND_AUTO_CREATE); }, Context.BIND_AUTO_CREATE);
} }
/**
* Update the displayed video streams based on the selected audio track.
*/
private void updateSecondaryStreams() {
final StreamSizeWrapper<AudioStream> audioStreams = getWrappedAudioStreams();
final var secondaryStreams = new SparseArrayCompat<SecondaryStreamHelper<AudioStream>>(4);
final List<VideoStream> videoStreams = wrappedVideoStreams.getStreamsList();
wrappedVideoStreams.resetSizes();
for (int i = 0; i < videoStreams.size(); i++) {
if (!videoStreams.get(i).isVideoOnly()) {
continue;
}
final AudioStream audioStream = SecondaryStreamHelper
.getAudioStreamFor(audioStreams.getStreamsList(), videoStreams.get(i));
if (audioStream != null) {
secondaryStreams.append(i, new SecondaryStreamHelper<>(audioStreams, audioStream));
} else if (DEBUG) {
final MediaFormat mediaFormat = videoStreams.get(i).getFormat();
if (mediaFormat != null) {
Log.w(TAG, "No audio stream candidates for video format "
+ mediaFormat.name());
} else {
Log.w(TAG, "No audio stream candidates for unknown video format");
}
}
}
this.videoStreamsAdapter = new StreamItemAdapter<>(wrappedVideoStreams, secondaryStreams);
this.audioStreamsAdapter = new StreamItemAdapter<>(audioStreams);
}
@Override @Override
public View onCreateView(@NonNull final LayoutInflater inflater, public View onCreateView(@NonNull final LayoutInflater inflater,
final ViewGroup container, final ViewGroup container,
@ -285,13 +307,13 @@ public class DownloadDialog extends DialogFragment
dialogBinding.fileName.setText(FilenameUtils.createFilename(getContext(), dialogBinding.fileName.setText(FilenameUtils.createFilename(getContext(),
currentInfo.getName())); currentInfo.getName()));
selectedAudioIndex = ListHelper selectedAudioIndex = ListHelper.getDefaultAudioFormat(getContext(),
.getDefaultAudioFormat(getContext(), wrappedAudioStreams.getStreamsList()); getWrappedAudioStreams().getStreamsList());
selectedSubtitleIndex = getSubtitleIndexBy(subtitleStreamsAdapter.getAll()); selectedSubtitleIndex = getSubtitleIndexBy(subtitleStreamsAdapter.getAll());
dialogBinding.qualitySpinner.setOnItemSelectedListener(this); dialogBinding.qualitySpinner.setOnItemSelectedListener(this);
dialogBinding.audioTrackSpinner.setOnItemSelectedListener(this);
dialogBinding.videoAudioGroup.setOnCheckedChangeListener(this); dialogBinding.videoAudioGroup.setOnCheckedChangeListener(this);
initToolbar(dialogBinding.toolbarLayout.toolbar); initToolbar(dialogBinding.toolbarLayout.toolbar);
@ -383,7 +405,7 @@ public class DownloadDialog extends DialogFragment
new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG, new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
"Downloading video stream size", "Downloading video stream size",
currentInfo.getServiceId())))); currentInfo.getServiceId()))));
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedAudioStreams) disposables.add(StreamSizeWrapper.fetchSizeForWrapper(getWrappedAudioStreams())
.subscribe(result -> { .subscribe(result -> {
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()
== R.id.audio_button) { == R.id.audio_button) {
@ -405,14 +427,28 @@ public class DownloadDialog extends DialogFragment
currentInfo.getServiceId())))); currentInfo.getServiceId()))));
} }
private void setupAudioTrackSpinner() {
if (getContext() == null) {
return;
}
dialogBinding.audioTrackSpinner.setAdapter(audioTrackAdapter);
dialogBinding.audioTrackSpinner.setSelection(selectedAudioTrackIndex);
}
private void setupAudioSpinner() { private void setupAudioSpinner() {
if (getContext() == null) { if (getContext() == null) {
return; return;
} }
dialogBinding.qualitySpinner.setAdapter(audioStreamsAdapter); dialogBinding.qualitySpinner.setVisibility(View.GONE);
dialogBinding.qualitySpinner.setSelection(selectedAudioIndex);
setRadioButtonsState(true); setRadioButtonsState(true);
dialogBinding.audioStreamSpinner.setAdapter(audioStreamsAdapter);
dialogBinding.audioStreamSpinner.setSelection(selectedAudioIndex);
dialogBinding.audioStreamSpinner.setVisibility(View.VISIBLE);
dialogBinding.audioTrackSpinner.setVisibility(
wrappedAudioTracks.size() > 1 ? View.VISIBLE : View.GONE);
dialogBinding.audioTrackPresentInVideoText.setVisibility(View.GONE);
} }
private void setupVideoSpinner() { private void setupVideoSpinner() {
@ -422,7 +458,19 @@ public class DownloadDialog extends DialogFragment
dialogBinding.qualitySpinner.setAdapter(videoStreamsAdapter); dialogBinding.qualitySpinner.setAdapter(videoStreamsAdapter);
dialogBinding.qualitySpinner.setSelection(selectedVideoIndex); dialogBinding.qualitySpinner.setSelection(selectedVideoIndex);
dialogBinding.qualitySpinner.setVisibility(View.VISIBLE);
setRadioButtonsState(true); setRadioButtonsState(true);
dialogBinding.audioStreamSpinner.setVisibility(View.GONE);
onVideoStreamSelected();
}
private void onVideoStreamSelected() {
final boolean isVideoOnly = videoStreamsAdapter.getItem(selectedVideoIndex).isVideoOnly();
dialogBinding.audioTrackSpinner.setVisibility(
isVideoOnly && wrappedAudioTracks.size() > 1 ? View.VISIBLE : View.GONE);
dialogBinding.audioTrackPresentInVideoText.setVisibility(
!isVideoOnly && wrappedAudioTracks.size() > 1 ? View.VISIBLE : View.GONE);
} }
private void setupSubtitleSpinner() { private void setupSubtitleSpinner() {
@ -432,7 +480,11 @@ public class DownloadDialog extends DialogFragment
dialogBinding.qualitySpinner.setAdapter(subtitleStreamsAdapter); dialogBinding.qualitySpinner.setAdapter(subtitleStreamsAdapter);
dialogBinding.qualitySpinner.setSelection(selectedSubtitleIndex); dialogBinding.qualitySpinner.setSelection(selectedSubtitleIndex);
dialogBinding.qualitySpinner.setVisibility(View.VISIBLE);
setRadioButtonsState(true); setRadioButtonsState(true);
dialogBinding.audioStreamSpinner.setVisibility(View.GONE);
dialogBinding.audioTrackSpinner.setVisibility(View.GONE);
dialogBinding.audioTrackPresentInVideoText.setVisibility(View.GONE);
} }
@ -550,18 +602,31 @@ public class DownloadDialog extends DialogFragment
+ "parent = [" + parent + "], view = [" + view + "], " + "parent = [" + parent + "], view = [" + view + "], "
+ "position = [" + position + "], id = [" + id + "]"); + "position = [" + position + "], id = [" + id + "]");
} }
switch (parent.getId()) {
case R.id.quality_spinner:
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) { switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
case R.id.audio_button:
selectedAudioIndex = position;
break;
case R.id.video_button: case R.id.video_button:
selectedVideoIndex = position; selectedVideoIndex = position;
onVideoStreamSelected();
break; break;
case R.id.subtitle_button: case R.id.subtitle_button:
selectedSubtitleIndex = position; selectedSubtitleIndex = position;
break; break;
} }
onItemSelectedSetFileName(); onItemSelectedSetFileName();
break;
case R.id.audio_track_spinner:
final boolean trackChanged = selectedAudioTrackIndex != position;
selectedAudioTrackIndex = position;
if (trackChanged) {
updateSecondaryStreams();
fetchStreamsSize();
}
break;
case R.id.audio_stream_spinner:
selectedAudioIndex = position;
}
} }
private void onItemSelectedSetFileName() { private void onItemSelectedSetFileName() {
@ -607,6 +672,7 @@ public class DownloadDialog extends DialogFragment
protected void setupDownloadOptions() { protected void setupDownloadOptions() {
setRadioButtonsState(false); setRadioButtonsState(false);
setupAudioTrackSpinner();
final boolean isVideoStreamsAvailable = videoStreamsAdapter.getCount() > 0; final boolean isVideoStreamsAvailable = videoStreamsAdapter.getCount() > 0;
final boolean isAudioStreamsAvailable = audioStreamsAdapter.getCount() > 0; final boolean isAudioStreamsAvailable = audioStreamsAdapter.getCount() > 0;
@ -657,6 +723,13 @@ public class DownloadDialog extends DialogFragment
dialogBinding.subtitleButton.setEnabled(enabled); dialogBinding.subtitleButton.setEnabled(enabled);
} }
private StreamSizeWrapper<AudioStream> getWrappedAudioStreams() {
if (selectedAudioTrackIndex < 0 || selectedAudioTrackIndex > wrappedAudioTracks.size()) {
return StreamSizeWrapper.empty();
}
return wrappedAudioTracks.getTracksList().get(selectedAudioTrackIndex);
}
private int getSubtitleIndexBy(@NonNull final List<SubtitlesStream> streams) { private int getSubtitleIndexBy(@NonNull final List<SubtitlesStream> streams) {
final Localization preferredLocalization = NewPipe.getPreferredLocalization(); final Localization preferredLocalization = NewPipe.getPreferredLocalization();
@ -1013,7 +1086,6 @@ public class DownloadDialog extends DialogFragment
psName = Postprocessing.ALGORITHM_WEBM_MUXER; psName = Postprocessing.ALGORITHM_WEBM_MUXER;
} }
psArgs = null;
final long videoSize = wrappedVideoStreams.getSizeInBytes( final long videoSize = wrappedVideoStreams.getSizeInBytes(
(VideoStream) selectedStream); (VideoStream) selectedStream);

View File

@ -162,8 +162,12 @@ public final class VideoDetailFragment
private boolean showRelatedItems; private boolean showRelatedItems;
private boolean showDescription; private boolean showDescription;
private String selectedTabTag; private String selectedTabTag;
@AttrRes @NonNull final List<Integer> tabIcons = new ArrayList<>(); @AttrRes
@StringRes @NonNull final List<Integer> tabContentDescriptions = new ArrayList<>(); @NonNull
final List<Integer> tabIcons = new ArrayList<>();
@StringRes
@NonNull
final List<Integer> tabContentDescriptions = new ArrayList<>();
private boolean tabSettingsChanged = false; private boolean tabSettingsChanged = false;
private int lastAppBarVerticalOffset = Integer.MAX_VALUE; // prevents useless updates private int lastAppBarVerticalOffset = Integer.MAX_VALUE; // prevents useless updates
@ -1040,20 +1044,10 @@ public final class VideoDetailFragment
player.setRecovery(); player.setRecovery();
} }
if (!useExternalAudioPlayer) { if (useExternalAudioPlayer) {
openNormalBackgroundPlayer(append); showExternalAudioPlaybackDialog();
} else { } else {
final List<AudioStream> audioStreams = getUrlAndNonTorrentStreams( openNormalBackgroundPlayer(append);
currentInfo.getAudioStreams());
final int index = ListHelper.getDefaultAudioFormat(activity, audioStreams);
if (index == -1) {
Toast.makeText(activity, R.string.no_audio_streams_available_for_external_players,
Toast.LENGTH_SHORT).show();
return;
}
startOnExternalPlayer(activity, currentInfo, audioStreams.get(index));
} }
} }
@ -1106,7 +1100,7 @@ public final class VideoDetailFragment
if (PreferenceManager.getDefaultSharedPreferences(activity) if (PreferenceManager.getDefaultSharedPreferences(activity)
.getBoolean(this.getString(R.string.use_external_video_player_key), false)) { .getBoolean(this.getString(R.string.use_external_video_player_key), false)) {
showExternalPlaybackDialog(); showExternalVideoPlaybackDialog();
} else { } else {
replaceQueueIfUserConfirms(this::openMainPlayer); replaceQueueIfUserConfirms(this::openMainPlayer);
} }
@ -2112,7 +2106,7 @@ public final class VideoDetailFragment
}).show(); }).show();
} }
private void showExternalPlaybackDialog() { private void showExternalVideoPlaybackDialog() {
if (currentInfo == null) { if (currentInfo == null) {
return; return;
} }
@ -2159,6 +2153,44 @@ public final class VideoDetailFragment
builder.show(); builder.show();
} }
private void showExternalAudioPlaybackDialog() {
if (currentInfo == null) {
return;
}
final List<AudioStream> audioStreams = getUrlAndNonTorrentStreams(
currentInfo.getAudioStreams());
final List<AudioStream> audioTracks =
ListHelper.getFilteredAudioStreams(activity, audioStreams);
if (audioTracks.isEmpty()) {
Toast.makeText(activity, R.string.no_audio_streams_available_for_external_players,
Toast.LENGTH_SHORT).show();
} else if (audioTracks.size() == 1) {
startOnExternalPlayer(activity, currentInfo, audioTracks.get(0));
} else {
final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setTitle(R.string.select_audio_track_external_players);
builder.setNeutralButton(R.string.open_in_browser, (dialog, i) ->
ShareUtils.openUrlInBrowser(requireActivity(), url));
final int selectedAudioStream =
ListHelper.getDefaultAudioFormat(activity, audioTracks);
final CharSequence[] trackNames = audioTracks.stream()
.map(audioStream -> Localization.audioTrackName(activity, audioStream))
.toArray(CharSequence[]::new);
builder.setSingleChoiceItems(trackNames, selectedAudioStream, null);
builder.setNegativeButton(R.string.cancel, null);
builder.setPositiveButton(R.string.ok, (dialog, i) -> {
final int index = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
startOnExternalPlayer(activity, currentInfo,
audioTracks.get(index));
});
builder.show();
}
}
/* /*
* Remove unneeded information while waiting for a next task * Remove unneeded information while waiting for a next task
* */ * */

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,9 @@ 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.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 +58,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 +105,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 +162,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 +606,69 @@ 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);
final Optional<AudioStream> selectedAudioStream = player.getSelectedAudioStream();
if (availableStreams == null || availableStreams.size() < 2
|| selectedAudioStream.isEmpty()) {
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);
audioTrackMenu.add(MENU_ID_AUDIO_TRACK, i, Menu.NONE,
Localization.audioTrackName(this, audioStream));
}
final AudioStream s = selectedAudioStream.get();
final String trackName = Localization.audioTrackName(this, s);
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) {
if (player.getCurrentMetadata() == null) {
return;
}
player.getCurrentMetadata().getMaybeAudioTrack().ifPresent(audioTrack -> {
final List<AudioStream> availableStreams = audioTrack.getAudioStreams();
final int selectedStreamIndex = audioTrack.getSelectedAudioStreamIndex();
if (selectedStreamIndex == itemId || availableStreams.size() <= itemId) {
return;
}
final String newAudioTrack = availableStreams.get(itemId).getAudioTrackId();
player.setAudioTrack(newAudioTrack);
});
}
} }

View File

@ -86,6 +86,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;
@ -179,13 +180,18 @@ public final class Player implements PlaybackListener, Listener {
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
// play queue might be null e.g. while player is starting // play queue might be null e.g. while player is starting
@Nullable private PlayQueue playQueue; @Nullable
private PlayQueue playQueue;
@Nullable private MediaSourceManager playQueueManager; @Nullable
private MediaSourceManager playQueueManager;
@Nullable private PlayQueueItem currentItem; @Nullable
@Nullable private MediaItemTag currentMetadata; private PlayQueueItem currentItem;
@Nullable private Bitmap currentThumbnail; @Nullable
private MediaItemTag currentMetadata;
@Nullable
private Bitmap currentThumbnail;
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Player // Player
@ -194,12 +200,17 @@ public final class Player implements PlaybackListener, Listener {
private ExoPlayer simpleExoPlayer; private ExoPlayer simpleExoPlayer;
private AudioReactor audioReactor; private AudioReactor audioReactor;
@NonNull private final DefaultTrackSelector trackSelector; @NonNull
@NonNull private final LoadController loadController; private final DefaultTrackSelector trackSelector;
@NonNull private final DefaultRenderersFactory renderFactory; @NonNull
private final LoadController loadController;
@NonNull
private final DefaultRenderersFactory renderFactory;
@NonNull private final VideoPlaybackResolver videoResolver; @NonNull
@NonNull private final AudioPlaybackResolver audioResolver; private final VideoPlaybackResolver videoResolver;
@NonNull
private final AudioPlaybackResolver audioResolver;
private final PlayerService service; //TODO try to remove and replace everything with context private final PlayerService service; //TODO try to remove and replace everything with context
@ -224,24 +235,32 @@ public final class Player implements PlaybackListener, Listener {
private BroadcastReceiver broadcastReceiver; private BroadcastReceiver broadcastReceiver;
private IntentFilter intentFilter; private IntentFilter intentFilter;
@Nullable private PlayerServiceEventListener fragmentListener = null; @Nullable
@Nullable private PlayerEventListener activityListener = null; private PlayerServiceEventListener fragmentListener = null;
@Nullable
private PlayerEventListener activityListener = null;
@NonNull private final SerialDisposable progressUpdateDisposable = new SerialDisposable(); @NonNull
@NonNull private final CompositeDisposable databaseUpdateDisposable = new CompositeDisposable(); private final SerialDisposable progressUpdateDisposable = new SerialDisposable();
@NonNull
private final CompositeDisposable databaseUpdateDisposable = new CompositeDisposable();
// This is the only listener we need for thumbnail loading, since there is always at most only // This is the only listener we need for thumbnail loading, since there is always at most only
// one thumbnail being loaded at a time. This field is also here to maintain a strong reference, // one thumbnail being loaded at a time. This field is also here to maintain a strong reference,
// which would otherwise be garbage collected since Picasso holds weak references to targets. // which would otherwise be garbage collected since Picasso holds weak references to targets.
@NonNull private final Target currentThumbnailTarget; @NonNull
private final Target currentThumbnailTarget;
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Utils // Utils
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@NonNull private final Context context; @NonNull
@NonNull private final SharedPreferences prefs; private final Context context;
@NonNull private final HistoryRecordManager recordManager; @NonNull
private final SharedPreferences prefs;
@NonNull
private final HistoryRecordManager recordManager;
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
@ -333,7 +352,7 @@ public final class Player implements PlaybackListener, Listener {
isAudioOnly = audioPlayerSelected(); isAudioOnly = audioPlayerSelected();
if (intent.hasExtra(PLAYBACK_QUALITY)) { if (intent.hasExtra(PLAYBACK_QUALITY)) {
setPlaybackQuality(intent.getStringExtra(PLAYBACK_QUALITY)); videoResolver.setPlaybackQuality(intent.getStringExtra(PLAYBACK_QUALITY));
} }
// Resolve enqueue intents // Resolve enqueue intents
@ -922,7 +941,6 @@ public final class Player implements PlaybackListener, Listener {
//endregion //endregion
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Playback states // Playback states
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -1244,6 +1262,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()) {
@ -1264,6 +1285,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();
} }
}); });
}); });
@ -1351,6 +1378,7 @@ public final class Player implements PlaybackListener, Listener {
// Errors // Errors
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
//region Errors //region Errors
/** /**
* Process exceptions produced by {@link com.google.android.exoplayer2.ExoPlayer ExoPlayer}. * Process exceptions produced by {@link com.google.android.exoplayer2.ExoPlayer ExoPlayer}.
* <p>There are multiple types of errors:</p> * <p>There are multiple types of errors:</p>
@ -1377,8 +1405,9 @@ public final class Player implements PlaybackListener, Listener {
* For any error above that is <b>not</b> explicitly <b>catchable</b>, the player will * For any error above that is <b>not</b> explicitly <b>catchable</b>, the player will
* create a notification so users are aware. * create a notification so users are aware.
* </ul> * </ul>
*
* @see com.google.android.exoplayer2.Player.Listener#onPlayerError(PlaybackException) * @see com.google.android.exoplayer2.Player.Listener#onPlayerError(PlaybackException)
* */ */
// Any error code not explicitly covered here are either unrelated to NewPipe use case // Any error code not explicitly covered here are either unrelated to NewPipe use case
// (e.g. DRM) or not recoverable (e.g. Decoder error). In both cases, the player should // (e.g. DRM) or not recoverable (e.g. Decoder error). In both cases, the player should
// shutdown. // shutdown.
@ -1760,6 +1789,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));
} }
@ -1888,6 +1918,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::getMaybeAudioTrack)
.map(MediaItemTag.AudioTrack::getSelectedAudioStream);
}
//endregion //endregion
@ -2019,6 +2055,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;
@ -2177,7 +2222,18 @@ public final class Player implements PlaybackListener, Listener {
} }
public void setPlaybackQuality(@Nullable final String quality) { public void setPlaybackQuality(@Nullable final String quality) {
saveStreamProgressState();
setRecovery();
videoResolver.setPlaybackQuality(quality); videoResolver.setPlaybackQuality(quality);
reloadPlayQueueManager();
}
public void setAudioTrack(@Nullable final String audioTrackId) {
saveStreamProgressState();
setRecovery();
videoResolver.setAudioTrack(audioTrackId);
audioResolver.setAudioTrack(audioTrackId);
reloadPlayQueueManager();
} }
@ -2255,7 +2311,7 @@ public final class Player implements PlaybackListener, Listener {
/** /**
* Get the video renderer index of the current playing stream. * Get the video renderer index of the current playing stream.
* * <p>
* This method returns the video renderer index of the current * This method returns the video renderer index of the current
* {@link MappingTrackSelector.MappedTrackInfo} or {@link #RENDERER_UNAVAILABLE} if the current * {@link MappingTrackSelector.MappedTrackInfo} or {@link #RENDERER_UNAVAILABLE} if the current
* {@link MappingTrackSelector.MappedTrackInfo} is null or if there is no video renderer index. * {@link MappingTrackSelector.MappedTrackInfo} is null or if there is no video renderer index.

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

@ -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<AudioTrack> getMaybeAudioTrack() {
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 AudioTrack {
@NonNull
private final List<AudioStream> audioStreams;
private final int selectedAudioStreamIndex;
private AudioTrack(@NonNull final List<AudioStream> audioStreams,
final int selectedAudioStreamIndex) {
this.audioStreams = audioStreams;
this.selectedAudioStreamIndex = selectedAudioStreamIndex;
}
static AudioTrack of(@NonNull final List<AudioStream> audioStreams,
final int selectedAudioStreamIndex) {
return new AudioTrack(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);
}
}
} }

View File

@ -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,41 @@ public final class StreamInfoTag implements MediaItemTag {
@Nullable @Nullable
private final MediaItemTag.Quality quality; private final MediaItemTag.Quality quality;
@Nullable @Nullable
private final MediaItemTag.AudioTrack audioTrack;
@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.AudioTrack audioTrack,
@Nullable final Object extras) { @Nullable final Object extras) {
this.streamInfo = streamInfo; this.streamInfo = streamInfo;
this.quality = quality; this.quality = quality;
this.audioTrack = audioTrack;
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 AudioTrack audioTrack =
AudioTrack.of(audioStreams, selectedAudioStreamIndex);
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) {
return new StreamInfoTag(streamInfo, null, null); return new StreamInfoTag(streamInfo, null, null, null);
} }
@Override @Override
@ -103,6 +120,12 @@ public final class StreamInfoTag implements MediaItemTag {
return Optional.ofNullable(quality); return Optional.ofNullable(quality);
} }
@NonNull
@Override
public Optional<AudioTrack> getMaybeAudioTrack() {
return Optional.ofNullable(audioTrack);
}
@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 +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, 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.getPlayableStreams; import static org.schabi.newpipe.util.ListHelper.getPlayableStreams;
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) {
@ -35,6 +38,13 @@ public class AudioPlaybackResolver implements PlaybackResolver {
this.dataSource = dataSource; this.dataSource = dataSource;
} }
/**
* Get a media source providing 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
*/
@Override @Override
@Nullable @Nullable
public MediaSource resolve(@NonNull final StreamInfo info) { public MediaSource resolve(@NonNull final StreamInfo info) {
@ -43,12 +53,27 @@ 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()) {
final int audioIndex =
ListHelper.getAudioFormatIndex(context, audioStreams, audioTrack);
stream = getStreamForIndex(audioIndex, audioStreams);
tag = StreamInfoTag.of(info, audioStreams, audioIndex);
} else {
final List<VideoStream> videoStreams =
getPlayableStreams(info.getVideoStreams(), info.getServiceId());
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,31 +84,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 = getPlayableStreams(
info.getAudioStreams(), info.getServiceId());
if (!audioStreams.isEmpty()) {
final int index = ListHelper.getDefaultAudioFormat(context, audioStreams);
return getStreamForIndex(index, audioStreams);
} else {
final List<VideoStream> videoStreams = getPlayableStreams(
info.getVideoStreams(), info.getServiceId());
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()) {
@ -91,4 +91,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

@ -156,6 +156,16 @@ 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());
}
if (audioStream.getAudioLocale() != null) {
cacheKey.append(" ");
cacheKey.append(audioStream.getAudioLocale().getISO3Language());
}
return cacheKey.toString(); return cacheKey.toString();
} }

View File

@ -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.getPlayableStreams; import static org.schabi.newpipe.util.ListHelper.getPlayableStreams;
@ -44,6 +45,8 @@ public class VideoPlaybackResolver implements PlaybackResolver {
@Nullable @Nullable
private String playbackQuality; private String playbackQuality;
@Nullable
private String audioTrack;
public enum SourceType { public enum SourceType {
LIVE_STREAM, LIVE_STREAM,
@ -74,19 +77,29 @@ public class VideoPlaybackResolver implements PlaybackResolver {
final List<VideoStream> videoStreamsList = ListHelper.getSortedStreamVideosList(context, final List<VideoStream> videoStreamsList = ListHelper.getSortedStreamVideosList(context,
getPlayableStreams(info.getVideoStreams(), info.getServiceId()), getPlayableStreams(info.getVideoStreams(), info.getServiceId()),
getPlayableStreams(info.getVideoOnlyStreams(), info.getServiceId()), false, true); getPlayableStreams(info.getVideoOnlyStreams(), info.getServiceId()), 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);
final int audioIndex =
ListHelper.getAudioFormatIndex(context, audioStreamsList, audioTrack);
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.getMaybeAudioTrack()
.map(MediaItemTag.AudioTrack::getSelectedAudioStream)
.orElse(null);
if (video != null) { if (video != null) {
try { try {
@ -99,15 +112,9 @@ public class VideoPlaybackResolver implements PlaybackResolver {
} }
} }
// Create optional audio stream source
final List<AudioStream> audioStreams = getPlayableStreams(
info.getAudioStreams(), info.getServiceId());
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() || 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);
@ -180,6 +187,15 @@ public class VideoPlaybackResolver implements PlaybackResolver {
this.playbackQuality = playbackQuality; this.playbackQuality = playbackQuality;
} }
@Nullable
public String getAudioTrack() {
return audioTrack;
}
public void setAudioTrack(@Nullable final String audioLanguage) {
this.audioTrack = audioLanguage;
}
public interface QualityResolver { public interface QualityResolver {
int getDefaultResolutionIndex(List<VideoStream> sortedVideos); int getDefaultResolutionIndex(List<VideoStream> sortedVideos);

View File

@ -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;
@ -78,6 +79,7 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper; import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper;
import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHolder; import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHolder;
import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.external_communication.KoreUtils; import org.schabi.newpipe.util.external_communication.KoreUtils;
import org.schabi.newpipe.util.external_communication.ShareUtils; import org.schabi.newpipe.util.external_communication.ShareUtils;
@ -108,7 +110,8 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
protected PlayerBinding binding; protected PlayerBinding binding;
private final Handler controlsVisibilityHandler = new Handler(Looper.getMainLooper()); private final Handler controlsVisibilityHandler = new Handler(Looper.getMainLooper());
@Nullable private SurfaceHolderCallback surfaceHolderCallback; @Nullable
private SurfaceHolderCallback surfaceHolderCallback;
boolean surfaceIsSetup = false; boolean surfaceIsSetup = false;
@ -117,11 +120,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_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 audioTrackPopupMenu;
protected PopupMenu playbackSpeedPopupMenu; protected PopupMenu playbackSpeedPopupMenu;
private PopupMenu captionPopupMenu; private PopupMenu captionPopupMenu;
@ -173,6 +178,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);
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);
@ -190,6 +196,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.audioTrackTextView.setOnClickListener(
makeOnClickListener(this::onAudioTracksClicked));
binding.playbackSpeed.setOnClickListener(makeOnClickListener(this::onPlaybackSpeedClicked)); binding.playbackSpeed.setOnClickListener(makeOnClickListener(this::onPlaybackSpeedClicked));
binding.playbackSeekBar.setOnSeekBarChangeListener(this); binding.playbackSeekBar.setOnSeekBarChangeListener(this);
@ -266,6 +274,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.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);
@ -419,6 +428,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.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);
@ -524,6 +534,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
/** /**
* Sets the current duration into the corresponding elements. * Sets the current duration into the corresponding elements.
*
* @param currentProgress the current progress, in milliseconds * @param currentProgress the current progress, in milliseconds
*/ */
private void updatePlayBackElementsCurrentDuration(final int currentProgress) { private void updatePlayBackElementsCurrentDuration(final int currentProgress) {
@ -536,6 +547,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
/** /**
* Sets the video duration time into all control components (e.g. seekbar). * Sets the video duration time into all control components (e.g. seekbar).
*
* @param duration the video duration, in milliseconds * @param duration the video duration, in milliseconds
*/ */
private void setVideoDurationToControls(final int duration) { private void setVideoDurationToControls(final int duration) {
@ -984,6 +996,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.audioTrackTextView.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 +1032,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
} }
buildQualityMenu(); buildQualityMenu();
buildAudioTrackMenu();
binding.qualityTextView.setVisibility(View.VISIBLE); binding.qualityTextView.setVisibility(View.VISIBLE);
binding.surfaceView.setVisibility(View.VISIBLE); binding.surfaceView.setVisibility(View.VISIBLE);
@ -1067,6 +1081,34 @@ 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 buildAudioTrackMenu() {
if (audioTrackPopupMenu == null) {
return;
}
audioTrackPopupMenu.getMenu().removeGroup(POPUP_MENU_ID_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) {
return;
}
for (int i = 0; i < availableStreams.size(); i++) {
final AudioStream audioStream = availableStreams.get(i);
audioTrackPopupMenu.getMenu().add(POPUP_MENU_ID_AUDIO_TRACK, i, Menu.NONE,
Localization.audioTrackName(context, audioStream));
}
player.getSelectedAudioStream()
.ifPresent(s -> binding.audioTrackTextView.setText(
Localization.audioTrackName(context, s)));
binding.audioTrackTextView.setVisibility(View.VISIBLE);
audioTrackPopupMenu.setOnMenuItemClickListener(this);
audioTrackPopupMenu.setOnDismissListener(this);
}
private void buildPlaybackSpeedMenu() { private void buildPlaybackSpeedMenu() {
if (playbackSpeedPopupMenu == null) { if (playbackSpeedPopupMenu == null) {
return; return;
@ -1175,6 +1217,11 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
.ifPresent(binding.qualityTextView::setText); .ifPresent(binding.qualityTextView::setText);
} }
private void onAudioTracksClicked() {
audioTrackPopupMenu.show();
isSomePopupMenuVisible = true;
}
/** /**
* 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.
*/ */
@ -1187,26 +1234,10 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
} }
if (menuItem.getGroupId() == POPUP_MENU_ID_QUALITY) { if (menuItem.getGroupId() == POPUP_MENU_ID_QUALITY) {
final int menuItemIndex = menuItem.getItemId(); onQualityItemClick(menuItem);
@Nullable final MediaItemTag currentMetadata = player.getCurrentMetadata();
if (currentMetadata == null || currentMetadata.getMaybeQuality().isEmpty()) {
return true; return true;
} } else if (menuItem.getGroupId() == POPUP_MENU_ID_AUDIO_TRACK) {
onAudioTrackItemClick(menuItem);
final MediaItemTag.Quality quality = currentMetadata.getMaybeQuality().get();
final List<VideoStream> availableStreams = quality.getSortedVideoStreams();
final int selectedStreamIndex = quality.getSelectedVideoStreamIndex();
if (selectedStreamIndex == menuItemIndex || availableStreams.size() <= menuItemIndex) {
return true;
}
player.saveStreamProgressState(); //TODO added, check if good
final String newResolution = availableStreams.get(menuItemIndex).getResolution();
player.setRecovery();
player.setPlaybackQuality(newResolution);
player.reloadPlayQueueManager();
binding.qualityTextView.setText(menuItem.getTitle());
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();
@ -1219,6 +1250,47 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
return false; return false;
} }
private void onQualityItemClick(@NonNull final MenuItem menuItem) {
final int menuItemIndex = menuItem.getItemId();
@Nullable final MediaItemTag currentMetadata = player.getCurrentMetadata();
if (currentMetadata == null || currentMetadata.getMaybeQuality().isEmpty()) {
return;
}
final MediaItemTag.Quality quality = currentMetadata.getMaybeQuality().get();
final List<VideoStream> availableStreams = quality.getSortedVideoStreams();
final int selectedStreamIndex = quality.getSelectedVideoStreamIndex();
if (selectedStreamIndex == menuItemIndex || availableStreams.size() <= menuItemIndex) {
return;
}
final String newResolution = availableStreams.get(menuItemIndex).getResolution();
player.setPlaybackQuality(newResolution);
binding.qualityTextView.setText(menuItem.getTitle());
}
private void onAudioTrackItemClick(@NonNull final MenuItem menuItem) {
final int menuItemIndex = menuItem.getItemId();
@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 == menuItemIndex || availableStreams.size() <= menuItemIndex) {
return;
}
final String newAudioTrack = availableStreams.get(menuItemIndex).getAudioTrackId();
player.setAudioTrack(newAudioTrack);
binding.audioTrackTextView.setText(menuItem.getTitle());
}
/** /**
* Called when some popup menu is dismissed. * Called when some popup menu is dismissed.
*/ */

View File

@ -0,0 +1,94 @@
package org.schabi.newpipe.util;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper;
import java.io.Serializable;
import java.util.List;
import java.util.stream.Collectors;
/**
* A list adapter for groups of {@link AudioStream}s (audio tracks).
*/
public class AudioTrackAdapter extends BaseAdapter {
private final AudioTracksWrapper tracksWrapper;
public AudioTrackAdapter(final AudioTracksWrapper tracksWrapper) {
this.tracksWrapper = tracksWrapper;
}
@Override
public int getCount() {
return tracksWrapper.size();
}
@Override
public List<AudioStream> getItem(final int position) {
return tracksWrapper.getTracksList().get(position).getStreamsList();
}
@Override
public long getItemId(final int position) {
return position;
}
@Override
public View getView(final int position, final View convertView, final ViewGroup parent) {
final var context = parent.getContext();
final View view;
if (convertView == null) {
view = LayoutInflater.from(context).inflate(
R.layout.stream_quality_item, parent, false);
} else {
view = convertView;
}
final ImageView woSoundIconView = view.findViewById(R.id.wo_sound_icon);
final TextView formatNameView = view.findViewById(R.id.stream_format_name);
final TextView qualityView = view.findViewById(R.id.stream_quality);
final TextView sizeView = view.findViewById(R.id.stream_size);
final List<AudioStream> streams = getItem(position);
final AudioStream stream = streams.get(0);
woSoundIconView.setVisibility(View.GONE);
sizeView.setVisibility(View.VISIBLE);
if (stream.getAudioTrackId() != null) {
formatNameView.setText(stream.getAudioTrackId());
}
qualityView.setText(Localization.audioTrackName(context, stream));
return view;
}
public static class AudioTracksWrapper implements Serializable {
private final List<StreamSizeWrapper<AudioStream>> tracksList;
public AudioTracksWrapper(@NonNull final List<List<AudioStream>> groupedAudioStreams,
@Nullable final Context context) {
this.tracksList = groupedAudioStreams.stream().map(streams ->
new StreamSizeWrapper<>(streams, context)).collect(Collectors.toList());
}
public List<StreamSizeWrapper<AudioStream>> getTracksList() {
return tracksList;
}
public int size() {
return tracksList.size();
}
}
}

View File

@ -15,6 +15,7 @@ import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
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.AudioStream;
import org.schabi.newpipe.extractor.stream.AudioTrackType;
import org.schabi.newpipe.extractor.stream.DeliveryMethod; import org.schabi.newpipe.extractor.stream.DeliveryMethod;
import org.schabi.newpipe.extractor.stream.Stream; import org.schabi.newpipe.extractor.stream.Stream;
import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.extractor.stream.VideoStream;
@ -25,6 +26,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;
@ -38,11 +40,17 @@ public final class ListHelper {
// Audio format in order of quality. 0=lowest quality, n=highest quality // Audio format in order of quality. 0=lowest quality, n=highest quality
private static final List<MediaFormat> AUDIO_FORMAT_QUALITY_RANKING = private static final List<MediaFormat> AUDIO_FORMAT_QUALITY_RANKING =
List.of(MediaFormat.MP3, MediaFormat.WEBMA, MediaFormat.M4A); List.of(MediaFormat.MP3, MediaFormat.WEBMA, MediaFormat.M4A);
// Audio format in order of efficiency. 0=most efficient, n=least efficient // Audio format in order of efficiency. 0=least efficient, n=most efficient
private static final List<MediaFormat> AUDIO_FORMAT_EFFICIENCY_RANKING = private static final List<MediaFormat> AUDIO_FORMAT_EFFICIENCY_RANKING =
List.of(MediaFormat.WEBMA, MediaFormat.M4A, MediaFormat.MP3); List.of(MediaFormat.MP3, MediaFormat.M4A, MediaFormat.WEBMA);
// 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");
// Audio track types in order of priotity. 0=lowest, n=highest
private static final List<AudioTrackType> AUDIO_TRACK_TYPE_RANKING =
List.of(AudioTrackType.DESCRIPTIVE, AudioTrackType.DUBBED, AudioTrackType.ORIGINAL);
// Audio track types in order of priotity when descriptive audio is preferred.
private static final List<AudioTrackType> AUDIO_TRACK_TYPE_RANKING_DESCRIPTIVE =
List.of(AudioTrackType.ORIGINAL, AudioTrackType.DUBBED, AudioTrackType.DESCRIPTIVE);
/** /**
* List of supported YouTube Itag ids. * List of supported YouTube Itag ids.
@ -62,10 +70,10 @@ public final class ListHelper {
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) {
@ -75,11 +83,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,
@ -88,10 +96,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) {
@ -101,11 +109,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,
@ -115,16 +123,36 @@ public final class ListHelper {
public static int getDefaultAudioFormat(final Context context, public static int getDefaultAudioFormat(final Context context,
final List<AudioStream> audioStreams) { final List<AudioStream> audioStreams) {
final MediaFormat defaultFormat = getDefaultFormat(context, return getAudioIndexByHighestRank(audioStreams,
R.string.default_audio_format_key, R.string.default_audio_format_value); getAudioTrackComparator(context).thenComparing(getAudioFormatComparator(context)));
// If the user has chosen to limit resolution to conserve mobile data
// usage then we should also limit our audio usage.
if (isLimitingDataUsage(context)) {
return getMostCompactAudioIndex(defaultFormat, audioStreams);
} else {
return getHighestQualityAudioIndex(defaultFormat, audioStreams);
} }
public static int getDefaultAudioTrackGroup(final Context context,
final List<List<AudioStream>> groupedAudioStreams) {
if (groupedAudioStreams == null || groupedAudioStreams.isEmpty()) {
return -1;
}
final Comparator<AudioStream> cmp = getAudioTrackComparator(context);
final List<AudioStream> highestRanked = groupedAudioStreams.stream()
.max((o1, o2) -> cmp.compare(o1.get(0), o2.get(0)))
.orElse(null);
return groupedAudioStreams.indexOf(highestRanked);
}
public static int getAudioFormatIndex(final Context context,
final List<AudioStream> audioStreams,
@Nullable final String trackId) {
if (trackId != null) {
for (int i = 0; i < audioStreams.size(); i++) {
final AudioStream s = audioStreams.get(i);
if (s.getAudioTrackId() != null
&& s.getAudioTrackId().equals(trackId)) {
return i;
}
}
}
return getDefaultAudioFormat(context, audioStreams);
} }
/** /**
@ -211,6 +239,90 @@ 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(
@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 = getAudioFormatComparator(context);
for (final AudioStream stream : audioStreams) {
if (stream.getDeliveryMethod() == DeliveryMethod.TORRENT) {
continue;
}
final String trackId = Objects.toString(stream.getAudioTrackId(), "");
final AudioStream presentStream = collectedStreams.get(trackId);
if (presentStream == null || cmp.compare(stream, presentStream) > 0) {
collectedStreams.put(trackId, stream);
}
}
// Filter unknown audio tracks if there are multiple tracks
if (collectedStreams.size() > 1) {
collectedStreams.remove("");
}
// Sort collected streams by name
return collectedStreams.values().stream().sorted(getAudioTrackNameComparator(context))
.collect(Collectors.toList());
}
/**
* Group the list of audioStreams by their track ID and sort the resulting list by track name.
*
* @param context app context to get track names for sorting
* @param audioStreams list of audio streams
* @return list of audio streams lists representing individual tracks
*/
public static List<List<AudioStream>> getGroupedAudioStreams(
@NonNull final Context context,
@Nullable final List<AudioStream> audioStreams) {
if (audioStreams == null) {
return Collections.emptyList();
}
final HashMap<String, List<AudioStream>> collectedStreams = new HashMap<>();
for (final AudioStream stream : audioStreams) {
final String trackId = Objects.toString(stream.getAudioTrackId(), "");
if (collectedStreams.containsKey(trackId)) {
collectedStreams.get(trackId).add(stream);
} else {
final List<AudioStream> list = new ArrayList<>();
list.add(stream);
collectedStreams.put(trackId, list);
}
}
// Filter unknown audio tracks if there are multiple tracks
if (collectedStreams.size() > 1) {
collectedStreams.remove("");
}
// Sort tracks alphabetically, sort track streams by quality
final Comparator<AudioStream> nameCmp = getAudioTrackNameComparator(context);
final Comparator<AudioStream> formatCmp = getAudioFormatComparator(context);
return collectedStreams.values().stream()
.sorted((o1, o2) -> nameCmp.compare(o1.get(0), o2.get(0)))
.map(streams -> streams.stream().sorted(formatCmp).collect(Collectors.toList()))
.collect(Collectors.toList());
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Utils // Utils
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -376,72 +488,22 @@ public final class ListHelper {
return videoStreams; return videoStreams;
} }
/**
* Get the audio from the list with the highest quality.
* Format will be ignored if it yields no results.
*
* @param format The target format type or null if it doesn't matter
* @param audioStreams List of audio streams
* @return Index of audio stream that produces the most compact results or -1 if not found
*/
static int getHighestQualityAudioIndex(@Nullable final MediaFormat format,
@Nullable final List<AudioStream> audioStreams) {
return getAudioIndexByHighestRank(format, audioStreams,
// Compares descending (last = highest rank)
getAudioStreamComparator(AUDIO_FORMAT_QUALITY_RANKING));
}
/**
* Get the audio from the list with the lowest bitrate and most efficient format.
* Format will be ignored if it yields no results.
*
* @param format The target format type or null if it doesn't matter
* @param audioStreams List of audio streams
* @return Index of audio stream that produces the most compact results or -1 if not found
*/
static int getMostCompactAudioIndex(@Nullable final MediaFormat format,
@Nullable final List<AudioStream> audioStreams) {
return getAudioIndexByHighestRank(format, audioStreams,
// The "reversed()" is important -> Compares ascending (first = highest rank)
getAudioStreamComparator(AUDIO_FORMAT_EFFICIENCY_RANKING).reversed());
}
private static Comparator<AudioStream> getAudioStreamComparator(
final List<MediaFormat> formatRanking) {
return Comparator.nullsLast(Comparator.comparingInt(AudioStream::getAverageBitrate))
.thenComparingInt(stream -> formatRanking.indexOf(stream.getFormat()));
}
/** /**
* Get the audio-stream from the list with the highest rank, depending on the comparator. * Get the audio-stream from the list with the highest rank, depending on the comparator.
* Format will be ignored if it yields no results. * Format will be ignored if it yields no results.
* *
* @param targetedFormat The target format type or null if it doesn't matter
* @param audioStreams List of audio streams * @param audioStreams List of audio streams
* @param comparator The comparator used for determining the max/best/highest ranked value * @param comparator The comparator used for determining the max/best/highest ranked value
* @return Index of audio stream that produces the highest ranked result or -1 if not found * @return Index of audio stream that produces the highest ranked result or -1 if not found
*/ */
private static int getAudioIndexByHighestRank(@Nullable final MediaFormat targetedFormat, static int getAudioIndexByHighestRank(@Nullable final List<AudioStream> audioStreams,
@Nullable final List<AudioStream> audioStreams,
final Comparator<AudioStream> comparator) { final Comparator<AudioStream> comparator) {
if (audioStreams == null || audioStreams.isEmpty()) { if (audioStreams == null || audioStreams.isEmpty()) {
return -1; return -1;
} }
final AudioStream highestRankedAudioStream = audioStreams.stream() final AudioStream highestRankedAudioStream = audioStreams.stream()
.filter(audioStream -> targetedFormat == null .max(comparator).orElse(null);
|| audioStream.getFormat() == targetedFormat)
.max(comparator)
.orElse(null);
if (highestRankedAudioStream == null) {
// Fallback: Ignore targetedFormat if not null
if (targetedFormat != null) {
return getAudioIndexByHighestRank(null, audioStreams, comparator);
}
// targetedFormat is already null -> return -1
return -1;
}
return audioStreams.indexOf(highestRankedAudioStream); return audioStreams.indexOf(highestRankedAudioStream);
} }
@ -629,4 +691,149 @@ public final class ListHelper {
return manager.isActiveNetworkMetered(); return manager.isActiveNetworkMetered();
} }
/**
* Get a {@link Comparator} to compare {@link AudioStream}s by their format and bitrate.
*
* <p>The prefered stream will be ordered last.</p>
*
* @param context app context
* @return Comparator
*/
private static Comparator<AudioStream> getAudioFormatComparator(
final @NonNull Context context) {
final MediaFormat defaultFormat = getDefaultFormat(context,
R.string.default_audio_format_key, R.string.default_audio_format_value);
return getAudioFormatComparator(defaultFormat, isLimitingDataUsage(context));
}
/**
* Get a {@link Comparator} to compare {@link AudioStream}s by their format and bitrate.
*
* <p>The prefered stream will be ordered last.</p>
*
* @param defaultFormat the default format to look for
* @param limitDataUsage choose low bitrate audio stream
* @return Comparator
*/
static Comparator<AudioStream> getAudioFormatComparator(
@Nullable final MediaFormat defaultFormat, final boolean limitDataUsage) {
final List<MediaFormat> formatRanking = limitDataUsage
? AUDIO_FORMAT_EFFICIENCY_RANKING : AUDIO_FORMAT_QUALITY_RANKING;
Comparator<AudioStream> bitrateComparator =
Comparator.comparingInt(AudioStream::getAverageBitrate);
if (limitDataUsage) {
bitrateComparator = bitrateComparator.reversed();
}
return Comparator.comparing(AudioStream::getFormat, (o1, o2) -> {
if (defaultFormat != null) {
return Boolean.compare(o1 == defaultFormat, o2 == defaultFormat);
}
return 0;
}).thenComparing(bitrateComparator).thenComparingInt(
stream -> formatRanking.indexOf(stream.getFormat()));
}
/**
* Get a {@link Comparator} to compare {@link AudioStream}s by their tracks.
*
* <p>Tracks will be compared this order:</p>
* <ol>
* <li>If {@code preferOriginalAudio}: use original audio</li>
* <li>Language matches {@code preferredLanguage}</li>
* <li>
* Track type ranks highest in this order:
* <i>Original</i> > <i>Dubbed</i> > <i>Descriptive</i>
* <p>If {@code preferDescriptiveAudio}:
* <i>Descriptive</i> > <i>Dubbed</i> > <i>Original</i></p>
* </li>
* <li>Language is English</li>
* </ol>
*
* <p>The prefered track will be ordered last.</p>
*
* @param context App context
* @return Comparator
*/
private static Comparator<AudioStream> getAudioTrackComparator(
@NonNull final Context context) {
final SharedPreferences preferences =
PreferenceManager.getDefaultSharedPreferences(context);
final Locale preferredLanguage = Localization.getPreferredLocale(context);
final boolean preferOriginalAudio =
preferences.getBoolean(context.getString(R.string.prefer_original_audio_key),
false);
final boolean preferDescriptiveAudio =
preferences.getBoolean(context.getString(R.string.prefer_descriptive_audio_key),
false);
return getAudioTrackComparator(preferredLanguage, preferOriginalAudio,
preferDescriptiveAudio);
}
/**
* Get a {@link Comparator} to compare {@link AudioStream}s by their tracks.
*
* <p>Tracks will be compared this order:</p>
* <ol>
* <li>If {@code preferOriginalAudio}: use original audio</li>
* <li>Language matches {@code preferredLanguage}</li>
* <li>
* Track type ranks highest in this order:
* <i>Original</i> > <i>Dubbed</i> > <i>Descriptive</i>
* <p>If {@code preferDescriptiveAudio}:
* <i>Descriptive</i> > <i>Dubbed</i> > <i>Original</i></p>
* </li>
* <li>Language is English</li>
* </ol>
*
* <p>The prefered track will be ordered last.</p>
*
* @param preferredLanguage Preferred audio stream language
* @param preferOriginalAudio Get the original audio track regardless of its language
* @param preferDescriptiveAudio Prefer the descriptive audio track if available
* @return Comparator
*/
static Comparator<AudioStream> getAudioTrackComparator(
final Locale preferredLanguage,
final boolean preferOriginalAudio,
final boolean preferDescriptiveAudio) {
final String langCode = preferredLanguage.getISO3Language();
final List<AudioTrackType> trackTypeRanking = preferDescriptiveAudio
? AUDIO_TRACK_TYPE_RANKING_DESCRIPTIVE : AUDIO_TRACK_TYPE_RANKING;
return Comparator.comparing(AudioStream::getAudioTrackType, (o1, o2) -> {
if (preferOriginalAudio) {
return Boolean.compare(
o1 == AudioTrackType.ORIGINAL, o2 == AudioTrackType.ORIGINAL);
}
return 0;
}).thenComparing(AudioStream::getAudioLocale,
Comparator.nullsFirst(Comparator.comparing(
locale -> locale.getISO3Language().equals(langCode))))
.thenComparing(AudioStream::getAudioTrackType,
Comparator.nullsFirst(Comparator.comparingInt(trackTypeRanking::indexOf)))
.thenComparing(AudioStream::getAudioLocale,
Comparator.nullsFirst(Comparator.comparing(
locale -> locale.getISO3Language().equals(
Locale.ENGLISH.getISO3Language()))));
}
/**
* Get a {@link Comparator} to compare {@link AudioStream}s by their languages and track types
* for alphabetical sorting.
*
* @param context app context for localization
* @return Comparator
*/
private static Comparator<AudioStream> getAudioTrackNameComparator(
@NonNull final Context context) {
final Locale appLoc = Localization.getAppLocale(context);
return Comparator.comparing(AudioStream::getAudioLocale, Comparator.nullsLast(
Comparator.comparing(locale -> locale.getDisplayName(appLoc))))
.thenComparing(AudioStream::getAudioTrackType);
}
} }

View File

@ -11,6 +11,7 @@ import android.text.TextUtils;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.PluralsRes; import androidx.annotation.PluralsRes;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import androidx.core.math.MathUtils; import androidx.core.math.MathUtils;
@ -21,6 +22,8 @@ import org.ocpsoft.prettytime.units.Decade;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.localization.ContentCountry; import org.schabi.newpipe.extractor.localization.ContentCountry;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.AudioTrackType;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode; import java.math.RoundingMode;
@ -261,6 +264,52 @@ public final class Localization {
} }
} }
/**
* Get the localized name of an audio track.
*
* <p>Examples of results returned by this method:</p>
* <ul>
* <li>English (original)</li>
* <li>English (descriptive)</li>
* <li>Spanish (dubbed)</li>
* </ul>
*
* @param context the context used to get the app language
* @param track an {@link AudioStream} of the track
* @return the localized name of the audio track
*/
public static String audioTrackName(final Context context, final AudioStream track) {
final String name;
if (track.getAudioLocale() != null) {
name = track.getAudioLocale().getDisplayLanguage(getAppLocale(context));
} else if (track.getAudioTrackName() != null) {
name = track.getAudioTrackName();
} else {
name = context.getString(R.string.unknown_audio_track);
}
if (track.getAudioTrackType() != null) {
final String trackType = audioTrackType(context, track.getAudioTrackType());
if (trackType != null) {
return context.getString(R.string.audio_track_name, name, trackType);
}
}
return name;
}
@Nullable
private static String audioTrackType(final Context context, final AudioTrackType trackType) {
switch (trackType) {
case ORIGINAL:
return context.getString(R.string.audio_track_type_original);
case DUBBED:
return context.getString(R.string.audio_track_type_dubbed);
case DESCRIPTIVE:
return context.getString(R.string.audio_track_type_descriptive);
}
return null;
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Pretty Time // Pretty Time
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/

View File

@ -224,6 +224,8 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
public static class StreamSizeWrapper<T extends Stream> implements Serializable { public static class StreamSizeWrapper<T extends Stream> implements Serializable {
private static final StreamSizeWrapper<Stream> EMPTY = private static final StreamSizeWrapper<Stream> EMPTY =
new StreamSizeWrapper<>(Collections.emptyList(), null); new StreamSizeWrapper<>(Collections.emptyList(), null);
private static final int SIZE_UNSET = -2;
private final List<T> streamsList; private final List<T> streamsList;
private final long[] streamSizes; private final long[] streamSizes;
private final String unknownSize; private final String unknownSize;
@ -235,7 +237,7 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
this.unknownSize = context == null this.unknownSize = context == null
? "--.-" : context.getString(R.string.unknown_content); ? "--.-" : context.getString(R.string.unknown_content);
Arrays.fill(streamSizes, -2); resetSizes();
} }
/** /**
@ -251,7 +253,7 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
final Callable<Boolean> fetchAndSet = () -> { final Callable<Boolean> fetchAndSet = () -> {
boolean hasChanged = false; boolean hasChanged = false;
for (final X stream : streamsWrapper.getStreamsList()) { for (final X stream : streamsWrapper.getStreamsList()) {
if (streamsWrapper.getSizeInBytes(stream) > -2) { if (streamsWrapper.getSizeInBytes(stream) > SIZE_UNSET) {
continue; continue;
} }
@ -269,6 +271,10 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
.onErrorReturnItem(true); .onErrorReturnItem(true);
} }
public void resetSizes() {
Arrays.fill(streamSizes, SIZE_UNSET);
}
public static <X extends Stream> StreamSizeWrapper<X> empty() { public static <X extends Stream> StreamSizeWrapper<X> empty() {
//noinspection unchecked //noinspection unchecked
return (StreamSizeWrapper<X>) EMPTY; return (StreamSizeWrapper<X>) EMPTY;

View File

@ -71,11 +71,45 @@
android:minWidth="150dp" android:minWidth="150dp"
tools:listitem="@layout/stream_quality_item" /> tools:listitem="@layout/stream_quality_item" />
<Spinner
android:id="@+id/audio_track_spinner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/quality_spinner"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:layout_marginBottom="12dp"
android:minWidth="150dp"
tools:visibility="gone" />
<Spinner
android:id="@+id/audio_stream_spinner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/audio_track_spinner"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:layout_marginBottom="12dp"
android:minWidth="150dp"
tools:visibility="gone" />
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/audio_track_present_in_video_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/audio_stream_spinner"
android:layout_marginLeft="24dp"
android:layout_marginRight="24dp"
android:layout_marginBottom="12dp"
android:gravity="center"
android:text="@string/audio_track_present_in_video"
android:textSize="12sp" />
<org.schabi.newpipe.views.NewPipeTextView <org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/threads_text_view" android:id="@+id/threads_text_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@+id/quality_spinner" android:layout_below="@+id/audio_track_present_in_video_text"
android:layout_marginLeft="24dp" android:layout_marginLeft="24dp"
android:layout_marginRight="24dp" android:layout_marginRight="24dp"
android:layout_marginBottom="6dp" android:layout_marginBottom="6dp"

View File

@ -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/audioTrackTextView"
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"

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

@ -219,6 +219,8 @@
<item>@string/none_control_key</item> <item>@string/none_control_key</item>
</string-array> </string-array>
<string name="prefer_original_audio_key">prefer_original_audio</string>
<string name="prefer_descriptive_audio_key">prefer_descriptive_audio</string>
<string name="last_resize_mode">last_resize_mode</string> <string name="last_resize_mode">last_resize_mode</string>
<!-- DEBUG ONLY --> <!-- DEBUG ONLY -->

View File

@ -94,6 +94,10 @@
<string name="show_description_summary">Turn off to hide video description and additional information</string> <string name="show_description_summary">Turn off to hide video description and additional information</string>
<string name="show_meta_info_title">Show meta info</string> <string name="show_meta_info_title">Show meta info</string>
<string name="show_meta_info_summary">Turn off to hide meta info boxes with additional information about the stream creator, stream content or a search request</string> <string name="show_meta_info_summary">Turn off to hide meta info boxes with additional information about the stream creator, stream content or a search request</string>
<string name="prefer_original_audio_title">Prefer original audio</string>
<string name="prefer_original_audio_summary">Select the original audio track regardless of the language</string>
<string name="prefer_descriptive_audio_title">Prefer descriptive audio</string>
<string name="prefer_descriptive_audio_summary">Select an audio track with descriptions for visually impaired people if available</string>
<string name="thumbnail_cache_wipe_complete_notice">Image cache wiped</string> <string name="thumbnail_cache_wipe_complete_notice">Image cache wiped</string>
<string name="metadata_cache_wipe_title">Wipe cached metadata</string> <string name="metadata_cache_wipe_title">Wipe cached metadata</string>
<string name="metadata_cache_wipe_summary">Remove all cached webpage data</string> <string name="metadata_cache_wipe_summary">Remove all cached webpage data</string>
@ -414,6 +418,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: %s</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>
@ -761,12 +767,15 @@
<string name="enumeration_comma">,</string> <string name="enumeration_comma">,</string>
<string name="toggle_all">Toggle all</string> <string name="toggle_all">Toggle all</string>
<string name="streams_not_yet_supported_removed">Streams which are not yet supported by the downloader are not shown</string> <string name="streams_not_yet_supported_removed">Streams which are not yet supported by the downloader are not shown</string>
<string name="audio_track_present_in_video">An audio track should be already present in this stream</string>
<string name="selected_stream_external_player_not_supported">The selected stream is not supported by external players</string> <string name="selected_stream_external_player_not_supported">The selected stream is not supported by external players</string>
<string name="no_audio_streams_available_for_external_players">No audio streams are available for external players</string> <string name="no_audio_streams_available_for_external_players">No audio streams are available for external players</string>
<string name="no_video_streams_available_for_external_players">No video streams are available for external players</string> <string name="no_video_streams_available_for_external_players">No video streams are available for external players</string>
<string name="select_quality_external_players">Select quality for external players</string> <string name="select_quality_external_players">Select quality for external players</string>
<string name="select_audio_track_external_players">Select audio track for external players</string>
<string name="unknown_format">Unknown format</string> <string name="unknown_format">Unknown format</string>
<string name="unknown_quality">Unknown quality</string> <string name="unknown_quality">Unknown quality</string>
<string name="unknown_audio_track">Unknown</string>
<string name="feed_toggle_show_future_items">Show future items</string> <string name="feed_toggle_show_future_items">Show future items</string>
<string name="feed_toggle_hide_future_items">Hide future items</string> <string name="feed_toggle_hide_future_items">Hide future items</string>
<string name="feed_show_watched">Fully watched</string> <string name="feed_show_watched">Fully watched</string>
@ -779,4 +788,8 @@
<string name="use_exoplayer_decoder_fallback_summary">Enable this option if you have decoder initialization issues, which falls back to lower-priority decoders if primary decoders initialization fail. This may result in poor playback performance than when using primary decoders</string> <string name="use_exoplayer_decoder_fallback_summary">Enable this option if you have decoder initialization issues, which falls back to lower-priority decoders if primary decoders initialization fail. This may result in poor playback performance than when using primary decoders</string>
<string name="always_use_exoplayer_set_output_surface_workaround_title">Always use ExoPlayer\'s video output surface setting workaround</string> <string name="always_use_exoplayer_set_output_surface_workaround_title">Always use ExoPlayer\'s video output surface setting workaround</string>
<string name="always_use_exoplayer_set_output_surface_workaround_summary">This workaround releases and re-instantiates video codecs when a surface change occurs, instead of setting the surface to the codec directly. Already used by ExoPlayer on some devices with this issue, this setting has only an effect on Android 6 and higher\n\nEnabling this option may prevent playback errors when switching the current video player or switching to fullscreen</string> <string name="always_use_exoplayer_set_output_surface_workaround_summary">This workaround releases and re-instantiates video codecs when a surface change occurs, instead of setting the surface to the codec directly. Already used by ExoPlayer on some devices with this issue, this setting has only an effect on Android 6 and higher\n\nEnabling this option may prevent playback errors when switching the current video player or switching to fullscreen</string>
<string name="audio_track_name">%s %s</string>
<string name="audio_track_type_original">original</string>
<string name="audio_track_type_dubbed">dubbed</string>
<string name="audio_track_type_descriptive">descriptive</string>
</resources> </resources>

View File

@ -61,6 +61,22 @@
app:iconSpaceReserved="false" app:iconSpaceReserved="false"
app:useSimpleSummaryProvider="true" /> app:useSimpleSummaryProvider="true" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="@string/prefer_original_audio_key"
android:summary="@string/prefer_original_audio_summary"
android:title="@string/prefer_original_audio_title"
app:singleLineTitle="false"
app:iconSpaceReserved="false"/>
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="@string/prefer_descriptive_audio_key"
android:summary="@string/prefer_descriptive_audio_summary"
android:title="@string/prefer_descriptive_audio_title"
app:singleLineTitle="false"
app:iconSpaceReserved="false"/>
<PreferenceScreen <PreferenceScreen
android:fragment="org.schabi.newpipe.settings.ExoPlayerSettingsFragment" android:fragment="org.schabi.newpipe.settings.ExoPlayerSettingsFragment"
android:key="@string/exoplayer_settings_key" android:key="@string/exoplayer_settings_key"

View File

@ -3,10 +3,13 @@ package org.schabi.newpipe.util;
import org.junit.Test; import org.junit.Test;
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.AudioStream;
import org.schabi.newpipe.extractor.stream.AudioTrackType;
import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.extractor.stream.VideoStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Locale;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
@ -29,6 +32,15 @@ public class ListHelperTest {
generateAudioStream("mp3-192", MediaFormat.MP3, 192), generateAudioStream("mp3-192", MediaFormat.MP3, 192),
generateAudioStream("webma-320", MediaFormat.WEBMA, 320)); generateAudioStream("webma-320", MediaFormat.WEBMA, 320));
private static final List<AudioStream> AUDIO_TRACKS_TEST_LIST = List.of(
generateAudioTrack("en.or", "en.or", Locale.ENGLISH, AudioTrackType.ORIGINAL),
generateAudioTrack("en.du", "en.du", Locale.ENGLISH, AudioTrackType.DUBBED),
generateAudioTrack("en.ds", "en.ds", Locale.ENGLISH, AudioTrackType.DESCRIPTIVE),
generateAudioTrack("unknown", null, null, null),
generateAudioTrack("de.du", "de.du", Locale.GERMAN, AudioTrackType.DUBBED),
generateAudioTrack("de.ds", "de.ds", Locale.GERMAN, AudioTrackType.DESCRIPTIVE)
);
private static final List<VideoStream> VIDEO_STREAMS_TEST_LIST = List.of( private static final List<VideoStream> VIDEO_STREAMS_TEST_LIST = List.of(
generateVideoStream("mpeg_4-720", MediaFormat.MPEG_4, "720p", false), generateVideoStream("mpeg_4-720", MediaFormat.MPEG_4, "720p", false),
generateVideoStream("v3gpp-240", MediaFormat.v3GPP, "240p", false), generateVideoStream("v3gpp-240", MediaFormat.v3GPP, "240p", false),
@ -199,24 +211,29 @@ public class ListHelperTest {
@Test @Test
public void getHighestQualityAudioFormatTest() { public void getHighestQualityAudioFormatTest() {
AudioStream stream = AUDIO_STREAMS_TEST_LIST.get(ListHelper.getHighestQualityAudioIndex( Comparator<AudioStream> cmp = ListHelper.getAudioFormatComparator(MediaFormat.M4A, false);
MediaFormat.M4A, AUDIO_STREAMS_TEST_LIST)); AudioStream stream = AUDIO_STREAMS_TEST_LIST.get(ListHelper.getAudioIndexByHighestRank(
AUDIO_STREAMS_TEST_LIST, cmp));
assertEquals(320, stream.getAverageBitrate()); assertEquals(320, stream.getAverageBitrate());
assertEquals(MediaFormat.M4A, stream.getFormat()); assertEquals(MediaFormat.M4A, stream.getFormat());
stream = AUDIO_STREAMS_TEST_LIST.get(ListHelper.getHighestQualityAudioIndex( cmp = ListHelper.getAudioFormatComparator(MediaFormat.WEBMA, false);
MediaFormat.WEBMA, AUDIO_STREAMS_TEST_LIST)); stream = AUDIO_STREAMS_TEST_LIST.get(ListHelper.getAudioIndexByHighestRank(
AUDIO_STREAMS_TEST_LIST, cmp));
assertEquals(320, stream.getAverageBitrate()); assertEquals(320, stream.getAverageBitrate());
assertEquals(MediaFormat.WEBMA, stream.getFormat()); assertEquals(MediaFormat.WEBMA, stream.getFormat());
stream = AUDIO_STREAMS_TEST_LIST.get(ListHelper.getHighestQualityAudioIndex( cmp = ListHelper.getAudioFormatComparator(MediaFormat.MP3, false);
MediaFormat.MP3, AUDIO_STREAMS_TEST_LIST)); stream = AUDIO_STREAMS_TEST_LIST.get(ListHelper.getAudioIndexByHighestRank(
AUDIO_STREAMS_TEST_LIST, cmp));
assertEquals(192, stream.getAverageBitrate()); assertEquals(192, stream.getAverageBitrate());
assertEquals(MediaFormat.MP3, stream.getFormat()); assertEquals(MediaFormat.MP3, stream.getFormat());
} }
@Test @Test
public void getHighestQualityAudioFormatPreferredAbsent() { public void getHighestQualityAudioFormatPreferredAbsent() {
final Comparator<AudioStream> cmp =
ListHelper.getAudioFormatComparator(MediaFormat.MP3, false);
////////////////////////////////////////// //////////////////////////////////////////
// Doesn't contain the preferred format // // Doesn't contain the preferred format //
@ -227,8 +244,7 @@ public class ListHelperTest {
generateAudioStream("webma-192", MediaFormat.WEBMA, 192)); generateAudioStream("webma-192", MediaFormat.WEBMA, 192));
// List doesn't contains this format // List doesn't contains this format
// It should fallback to the highest bitrate audio no matter what format it is // It should fallback to the highest bitrate audio no matter what format it is
AudioStream stream = testList.get(ListHelper.getHighestQualityAudioIndex( AudioStream stream = testList.get(ListHelper.getAudioIndexByHighestRank(testList, cmp));
MediaFormat.MP3, testList));
assertEquals(192, stream.getAverageBitrate()); assertEquals(192, stream.getAverageBitrate());
assertEquals(MediaFormat.WEBMA, stream.getFormat()); assertEquals(MediaFormat.WEBMA, stream.getFormat());
@ -246,44 +262,51 @@ public class ListHelperTest {
generateAudioStream("webma-192-4", MediaFormat.WEBMA, 192))); generateAudioStream("webma-192-4", MediaFormat.WEBMA, 192)));
// List doesn't contains this format, it should fallback to the highest bitrate audio and // List doesn't contains this format, it should fallback to the highest bitrate audio and
// the highest quality format. // the highest quality format.
stream = testList.get(ListHelper.getHighestQualityAudioIndex(MediaFormat.MP3, testList)); stream =
testList.get(ListHelper.getAudioIndexByHighestRank(testList, cmp));
assertEquals(192, stream.getAverageBitrate()); assertEquals(192, stream.getAverageBitrate());
assertEquals(MediaFormat.M4A, stream.getFormat()); assertEquals(MediaFormat.M4A, stream.getFormat());
// Adding a new format and bitrate. Adding another stream will have no impact since // Adding a new format and bitrate. Adding another stream will have no impact since
// it's not a preferred format. // it's not a preferred format.
testList.add(generateAudioStream("webma-192-5", MediaFormat.WEBMA, 192)); testList.add(generateAudioStream("webma-192-5", MediaFormat.WEBMA, 192));
stream = testList.get(ListHelper.getHighestQualityAudioIndex(MediaFormat.MP3, testList)); stream =
testList.get(ListHelper.getAudioIndexByHighestRank(testList, cmp));
assertEquals(192, stream.getAverageBitrate()); assertEquals(192, stream.getAverageBitrate());
assertEquals(MediaFormat.M4A, stream.getFormat()); assertEquals(MediaFormat.M4A, stream.getFormat());
} }
@Test @Test
public void getHighestQualityAudioNull() { public void getHighestQualityAudioNull() {
assertEquals(-1, ListHelper.getHighestQualityAudioIndex(null, null)); final Comparator<AudioStream> cmp = ListHelper.getAudioFormatComparator(null, false);
assertEquals(-1, ListHelper.getHighestQualityAudioIndex(null, new ArrayList<>())); assertEquals(-1, ListHelper.getAudioIndexByHighestRank(null, cmp));
assertEquals(-1, ListHelper.getAudioIndexByHighestRank(new ArrayList<>(), cmp));
} }
@Test @Test
public void getLowestQualityAudioFormatTest() { public void getLowestQualityAudioFormatTest() {
AudioStream stream = AUDIO_STREAMS_TEST_LIST.get(ListHelper.getMostCompactAudioIndex( Comparator<AudioStream> cmp = ListHelper.getAudioFormatComparator(MediaFormat.M4A, true);
MediaFormat.M4A, AUDIO_STREAMS_TEST_LIST)); AudioStream stream = AUDIO_STREAMS_TEST_LIST.get(ListHelper.getAudioIndexByHighestRank(
AUDIO_STREAMS_TEST_LIST, cmp));
assertEquals(128, stream.getAverageBitrate()); assertEquals(128, stream.getAverageBitrate());
assertEquals(MediaFormat.M4A, stream.getFormat()); assertEquals(MediaFormat.M4A, stream.getFormat());
stream = AUDIO_STREAMS_TEST_LIST.get(ListHelper.getMostCompactAudioIndex( cmp = ListHelper.getAudioFormatComparator(MediaFormat.WEBMA, true);
MediaFormat.WEBMA, AUDIO_STREAMS_TEST_LIST)); stream = AUDIO_STREAMS_TEST_LIST.get(ListHelper.getAudioIndexByHighestRank(
AUDIO_STREAMS_TEST_LIST, cmp));
assertEquals(64, stream.getAverageBitrate()); assertEquals(64, stream.getAverageBitrate());
assertEquals(MediaFormat.WEBMA, stream.getFormat()); assertEquals(MediaFormat.WEBMA, stream.getFormat());
stream = AUDIO_STREAMS_TEST_LIST.get(ListHelper.getMostCompactAudioIndex( cmp = ListHelper.getAudioFormatComparator(MediaFormat.MP3, true);
MediaFormat.MP3, AUDIO_STREAMS_TEST_LIST)); stream = AUDIO_STREAMS_TEST_LIST.get(ListHelper.getAudioIndexByHighestRank(
AUDIO_STREAMS_TEST_LIST, cmp));
assertEquals(64, stream.getAverageBitrate()); assertEquals(64, stream.getAverageBitrate());
assertEquals(MediaFormat.MP3, stream.getFormat()); assertEquals(MediaFormat.MP3, stream.getFormat());
} }
@Test @Test
public void getLowestQualityAudioFormatPreferredAbsent() { public void getLowestQualityAudioFormatPreferredAbsent() {
Comparator<AudioStream> cmp = ListHelper.getAudioFormatComparator(MediaFormat.MP3, true);
////////////////////////////////////////// //////////////////////////////////////////
// Doesn't contain the preferred format // // Doesn't contain the preferred format //
@ -294,14 +317,13 @@ public class ListHelperTest {
generateAudioStream("webma-192-1", MediaFormat.WEBMA, 192))); generateAudioStream("webma-192-1", MediaFormat.WEBMA, 192)));
// List doesn't contains this format // List doesn't contains this format
// It should fallback to the most compact audio no matter what format it is. // It should fallback to the most compact audio no matter what format it is.
AudioStream stream = testList.get(ListHelper.getMostCompactAudioIndex( AudioStream stream = testList.get(ListHelper.getAudioIndexByHighestRank(testList, cmp));
MediaFormat.MP3, testList));
assertEquals(128, stream.getAverageBitrate()); assertEquals(128, stream.getAverageBitrate());
assertEquals(MediaFormat.M4A, stream.getFormat()); assertEquals(MediaFormat.M4A, stream.getFormat());
// WEBMA is more compact than M4A // WEBMA is more compact than M4A
testList.add(generateAudioStream("webma-192-2", MediaFormat.WEBMA, 128)); testList.add(generateAudioStream("webma-192-2", MediaFormat.WEBMA, 128));
stream = testList.get(ListHelper.getMostCompactAudioIndex(MediaFormat.MP3, testList)); stream = testList.get(ListHelper.getAudioIndexByHighestRank(testList, cmp));
assertEquals(128, stream.getAverageBitrate()); assertEquals(128, stream.getAverageBitrate());
assertEquals(MediaFormat.WEBMA, stream.getFormat()); assertEquals(MediaFormat.WEBMA, stream.getFormat());
@ -318,20 +340,58 @@ public class ListHelperTest {
generateAudioStream("m4a-192-3", MediaFormat.M4A, 192))); generateAudioStream("m4a-192-3", MediaFormat.M4A, 192)));
// List doesn't contain this format // List doesn't contain this format
// It should fallback to the most compact audio no matter what format it is. // It should fallback to the most compact audio no matter what format it is.
stream = testList.get(ListHelper.getMostCompactAudioIndex(MediaFormat.MP3, testList)); stream = testList.get(
ListHelper.getAudioIndexByHighestRank(testList, cmp));
assertEquals(192, stream.getAverageBitrate()); assertEquals(192, stream.getAverageBitrate());
assertEquals(MediaFormat.WEBMA, stream.getFormat()); assertEquals(MediaFormat.WEBMA, stream.getFormat());
// Should be same as above // Should be same as above
stream = testList.get(ListHelper.getMostCompactAudioIndex(null, testList)); cmp = ListHelper.getAudioFormatComparator(null, true);
stream = testList.get(
ListHelper.getAudioIndexByHighestRank(testList, cmp));
assertEquals(192, stream.getAverageBitrate()); assertEquals(192, stream.getAverageBitrate());
assertEquals(MediaFormat.WEBMA, stream.getFormat()); assertEquals(MediaFormat.WEBMA, stream.getFormat());
} }
@Test @Test
public void getLowestQualityAudioNull() { public void getLowestQualityAudioNull() {
assertEquals(-1, ListHelper.getMostCompactAudioIndex(null, null)); final Comparator<AudioStream> cmp = ListHelper.getAudioFormatComparator(null, false);
assertEquals(-1, ListHelper.getMostCompactAudioIndex(null, new ArrayList<>())); assertEquals(-1, ListHelper.getAudioIndexByHighestRank(null, cmp));
assertEquals(-1, ListHelper.getAudioIndexByHighestRank(new ArrayList<>(), cmp));
}
@Test
public void getAudioTrack() {
// English language
Comparator<AudioStream> cmp =
ListHelper.getAudioTrackComparator(Locale.ENGLISH, false, false);
AudioStream stream = AUDIO_TRACKS_TEST_LIST.get(ListHelper.getAudioIndexByHighestRank(
AUDIO_TRACKS_TEST_LIST, cmp));
assertEquals("en.or", stream.getId());
// German language
cmp = ListHelper.getAudioTrackComparator(Locale.GERMAN, false, false);
stream = AUDIO_TRACKS_TEST_LIST.get(ListHelper.getAudioIndexByHighestRank(
AUDIO_TRACKS_TEST_LIST, cmp));
assertEquals("de.du", stream.getId());
// German language, but prefer original
cmp = ListHelper.getAudioTrackComparator(Locale.GERMAN, true, false);
stream = AUDIO_TRACKS_TEST_LIST.get(ListHelper.getAudioIndexByHighestRank(
AUDIO_TRACKS_TEST_LIST, cmp));
assertEquals("en.or", stream.getId());
// Prefer descriptive audio
cmp = ListHelper.getAudioTrackComparator(Locale.ENGLISH, false, true);
stream = AUDIO_TRACKS_TEST_LIST.get(ListHelper.getAudioIndexByHighestRank(
AUDIO_TRACKS_TEST_LIST, cmp));
assertEquals("en.ds", stream.getId());
// Japanese language, fall back to original
cmp = ListHelper.getAudioTrackComparator(Locale.JAPANESE, true, false);
stream = AUDIO_TRACKS_TEST_LIST.get(ListHelper.getAudioIndexByHighestRank(
AUDIO_TRACKS_TEST_LIST, cmp));
assertEquals("en.or", stream.getId());
} }
@Test @Test
@ -390,6 +450,22 @@ public class ListHelperTest {
.build(); .build();
} }
private static AudioStream generateAudioTrack(
@NonNull final String id,
@Nullable final String trackId,
@Nullable final Locale locale,
@Nullable final AudioTrackType trackType) {
return new AudioStream.Builder()
.setId(id)
.setContent("", true)
.setMediaFormat(MediaFormat.M4A)
.setAverageBitrate(128)
.setAudioTrackId(trackId)
.setAudioLocale(locale)
.setAudioTrackType(trackType)
.build();
}
@NonNull @NonNull
private static VideoStream generateVideoStream(@NonNull final String id, private static VideoStream generateVideoStream(@NonNull final String id,
@Nullable final MediaFormat mediaFormat, @Nullable final MediaFormat mediaFormat,