Merge pull request #9937 from Theta-Dev/alang-selector
Add support for multiple audio tracks
This commit is contained in:
commit
2315b082ff
|
@ -68,6 +68,8 @@ import org.schabi.newpipe.util.SecondaryStreamHelper;
|
|||
import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener;
|
||||
import org.schabi.newpipe.util.StreamItemAdapter;
|
||||
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 java.io.File;
|
||||
|
@ -95,12 +97,14 @@ public class DownloadDialog extends DialogFragment
|
|||
@State
|
||||
StreamInfo currentInfo;
|
||||
@State
|
||||
StreamSizeWrapper<AudioStream> wrappedAudioStreams;
|
||||
@State
|
||||
StreamSizeWrapper<VideoStream> wrappedVideoStreams;
|
||||
@State
|
||||
StreamSizeWrapper<SubtitlesStream> wrappedSubtitleStreams;
|
||||
@State
|
||||
AudioTracksWrapper wrappedAudioTracks;
|
||||
@State
|
||||
int selectedAudioTrackIndex;
|
||||
@State
|
||||
int selectedVideoIndex; // set in the constructor
|
||||
@State
|
||||
int selectedAudioIndex = 0; // default to the first item
|
||||
|
@ -117,6 +121,7 @@ public class DownloadDialog extends DialogFragment
|
|||
private Context context;
|
||||
private boolean askForSavePath;
|
||||
|
||||
private AudioTrackAdapter audioTrackAdapter;
|
||||
private StreamItemAdapter<AudioStream, Stream> audioStreamsAdapter;
|
||||
private StreamItemAdapter<VideoStream, AudioStream> videoStreamsAdapter;
|
||||
private StreamItemAdapter<SubtitlesStream, Stream> subtitleStreamsAdapter;
|
||||
|
@ -163,18 +168,26 @@ public class DownloadDialog extends DialogFragment
|
|||
public DownloadDialog(@NonNull final Context context, @NonNull final StreamInfo 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
|
||||
final List<VideoStream> videoStreams = ListHelper.getSortedStreamVideosList(
|
||||
context,
|
||||
getStreamsOfSpecifiedDelivery(info.getVideoStreams(), PROGRESSIVE_HTTP),
|
||||
getStreamsOfSpecifiedDelivery(info.getVideoOnlyStreams(), PROGRESSIVE_HTTP),
|
||||
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.wrappedAudioStreams = new StreamSizeWrapper<>(
|
||||
getStreamsOfSpecifiedDelivery(info.getAudioStreams(), PROGRESSIVE_HTTP), context);
|
||||
this.wrappedSubtitleStreams = new StreamSizeWrapper<>(
|
||||
getStreamsOfSpecifiedDelivery(info.getSubtitles(), PROGRESSIVE_HTTP), context);
|
||||
|
||||
|
@ -212,33 +225,9 @@ public class DownloadDialog extends DialogFragment
|
|||
setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context));
|
||||
Icepick.restoreInstanceState(this, savedInstanceState);
|
||||
|
||||
final var secondaryStreams = new SparseArrayCompat<SecondaryStreamHelper<AudioStream>>(4);
|
||||
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.audioTrackAdapter = new AudioTrackAdapter(wrappedAudioTracks);
|
||||
this.subtitleStreamsAdapter = new StreamItemAdapter<>(wrappedSubtitleStreams);
|
||||
updateSecondaryStreams();
|
||||
|
||||
final Intent intent = new Intent(context, DownloadManagerService.class);
|
||||
context.startService(intent);
|
||||
|
@ -265,6 +254,39 @@ public class DownloadDialog extends DialogFragment
|
|||
}, 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
|
||||
public View onCreateView(@NonNull final LayoutInflater inflater,
|
||||
final ViewGroup container,
|
||||
|
@ -285,13 +307,13 @@ public class DownloadDialog extends DialogFragment
|
|||
|
||||
dialogBinding.fileName.setText(FilenameUtils.createFilename(getContext(),
|
||||
currentInfo.getName()));
|
||||
selectedAudioIndex = ListHelper
|
||||
.getDefaultAudioFormat(getContext(), wrappedAudioStreams.getStreamsList());
|
||||
selectedAudioIndex = ListHelper.getDefaultAudioFormat(getContext(),
|
||||
getWrappedAudioStreams().getStreamsList());
|
||||
|
||||
selectedSubtitleIndex = getSubtitleIndexBy(subtitleStreamsAdapter.getAll());
|
||||
|
||||
dialogBinding.qualitySpinner.setOnItemSelectedListener(this);
|
||||
|
||||
dialogBinding.audioTrackSpinner.setOnItemSelectedListener(this);
|
||||
dialogBinding.videoAudioGroup.setOnCheckedChangeListener(this);
|
||||
|
||||
initToolbar(dialogBinding.toolbarLayout.toolbar);
|
||||
|
@ -383,7 +405,7 @@ public class DownloadDialog extends DialogFragment
|
|||
new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
|
||||
"Downloading video stream size",
|
||||
currentInfo.getServiceId()))));
|
||||
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedAudioStreams)
|
||||
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(getWrappedAudioStreams())
|
||||
.subscribe(result -> {
|
||||
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()
|
||||
== R.id.audio_button) {
|
||||
|
@ -405,14 +427,28 @@ public class DownloadDialog extends DialogFragment
|
|||
currentInfo.getServiceId()))));
|
||||
}
|
||||
|
||||
private void setupAudioTrackSpinner() {
|
||||
if (getContext() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
dialogBinding.audioTrackSpinner.setAdapter(audioTrackAdapter);
|
||||
dialogBinding.audioTrackSpinner.setSelection(selectedAudioTrackIndex);
|
||||
}
|
||||
|
||||
private void setupAudioSpinner() {
|
||||
if (getContext() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
dialogBinding.qualitySpinner.setAdapter(audioStreamsAdapter);
|
||||
dialogBinding.qualitySpinner.setSelection(selectedAudioIndex);
|
||||
dialogBinding.qualitySpinner.setVisibility(View.GONE);
|
||||
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() {
|
||||
|
@ -422,7 +458,19 @@ public class DownloadDialog extends DialogFragment
|
|||
|
||||
dialogBinding.qualitySpinner.setAdapter(videoStreamsAdapter);
|
||||
dialogBinding.qualitySpinner.setSelection(selectedVideoIndex);
|
||||
dialogBinding.qualitySpinner.setVisibility(View.VISIBLE);
|
||||
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() {
|
||||
|
@ -432,7 +480,11 @@ public class DownloadDialog extends DialogFragment
|
|||
|
||||
dialogBinding.qualitySpinner.setAdapter(subtitleStreamsAdapter);
|
||||
dialogBinding.qualitySpinner.setSelection(selectedSubtitleIndex);
|
||||
dialogBinding.qualitySpinner.setVisibility(View.VISIBLE);
|
||||
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 + "], "
|
||||
+ "position = [" + position + "], id = [" + id + "]");
|
||||
}
|
||||
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
|
||||
case R.id.audio_button:
|
||||
|
||||
switch (parent.getId()) {
|
||||
case R.id.quality_spinner:
|
||||
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
|
||||
case R.id.video_button:
|
||||
selectedVideoIndex = position;
|
||||
onVideoStreamSelected();
|
||||
break;
|
||||
case R.id.subtitle_button:
|
||||
selectedSubtitleIndex = position;
|
||||
break;
|
||||
}
|
||||
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;
|
||||
break;
|
||||
case R.id.video_button:
|
||||
selectedVideoIndex = position;
|
||||
break;
|
||||
case R.id.subtitle_button:
|
||||
selectedSubtitleIndex = position;
|
||||
break;
|
||||
}
|
||||
onItemSelectedSetFileName();
|
||||
}
|
||||
|
||||
private void onItemSelectedSetFileName() {
|
||||
|
@ -607,6 +672,7 @@ public class DownloadDialog extends DialogFragment
|
|||
|
||||
protected void setupDownloadOptions() {
|
||||
setRadioButtonsState(false);
|
||||
setupAudioTrackSpinner();
|
||||
|
||||
final boolean isVideoStreamsAvailable = videoStreamsAdapter.getCount() > 0;
|
||||
final boolean isAudioStreamsAvailable = audioStreamsAdapter.getCount() > 0;
|
||||
|
@ -657,6 +723,13 @@ public class DownloadDialog extends DialogFragment
|
|||
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) {
|
||||
final Localization preferredLocalization = NewPipe.getPreferredLocalization();
|
||||
|
||||
|
@ -1013,7 +1086,6 @@ public class DownloadDialog extends DialogFragment
|
|||
psName = Postprocessing.ALGORITHM_WEBM_MUXER;
|
||||
}
|
||||
|
||||
psArgs = null;
|
||||
final long videoSize = wrappedVideoStreams.getSizeInBytes(
|
||||
(VideoStream) selectedStream);
|
||||
|
||||
|
|
|
@ -162,8 +162,12 @@ public final class VideoDetailFragment
|
|||
private boolean showRelatedItems;
|
||||
private boolean showDescription;
|
||||
private String selectedTabTag;
|
||||
@AttrRes @NonNull final List<Integer> tabIcons = new ArrayList<>();
|
||||
@StringRes @NonNull final List<Integer> tabContentDescriptions = new ArrayList<>();
|
||||
@AttrRes
|
||||
@NonNull
|
||||
final List<Integer> tabIcons = new ArrayList<>();
|
||||
@StringRes
|
||||
@NonNull
|
||||
final List<Integer> tabContentDescriptions = new ArrayList<>();
|
||||
private boolean tabSettingsChanged = false;
|
||||
private int lastAppBarVerticalOffset = Integer.MAX_VALUE; // prevents useless updates
|
||||
|
||||
|
@ -1040,20 +1044,10 @@ public final class VideoDetailFragment
|
|||
player.setRecovery();
|
||||
}
|
||||
|
||||
if (!useExternalAudioPlayer) {
|
||||
openNormalBackgroundPlayer(append);
|
||||
if (useExternalAudioPlayer) {
|
||||
showExternalAudioPlaybackDialog();
|
||||
} else {
|
||||
final List<AudioStream> audioStreams = getUrlAndNonTorrentStreams(
|
||||
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));
|
||||
openNormalBackgroundPlayer(append);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1106,7 +1100,7 @@ public final class VideoDetailFragment
|
|||
|
||||
if (PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
.getBoolean(this.getString(R.string.use_external_video_player_key), false)) {
|
||||
showExternalPlaybackDialog();
|
||||
showExternalVideoPlaybackDialog();
|
||||
} else {
|
||||
replaceQueueIfUserConfirms(this::openMainPlayer);
|
||||
}
|
||||
|
@ -2112,7 +2106,7 @@ public final class VideoDetailFragment
|
|||
}).show();
|
||||
}
|
||||
|
||||
private void showExternalPlaybackDialog() {
|
||||
private void showExternalVideoPlaybackDialog() {
|
||||
if (currentInfo == null) {
|
||||
return;
|
||||
}
|
||||
|
@ -2159,6 +2153,44 @@ public final class VideoDetailFragment
|
|||
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
|
||||
* */
|
||||
|
|
|
@ -13,6 +13,7 @@ import android.provider.Settings;
|
|||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.SubMenu;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.SeekBar;
|
||||
|
@ -27,11 +28,13 @@ import com.google.android.exoplayer2.PlaybackParameters;
|
|||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.ActivityPlayerQueueControlBinding;
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||
import org.schabi.newpipe.player.event.PlayerEventListener;
|
||||
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.PlayQueueAdapter;
|
||||
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.ThemeHelper;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public final class PlayQueueActivity extends AppCompatActivity
|
||||
implements PlayerEventListener, SeekBar.OnSeekBarChangeListener,
|
||||
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 MENU_ID_AUDIO_TRACK = 71;
|
||||
|
||||
private Player player;
|
||||
|
||||
private boolean serviceBound;
|
||||
|
@ -97,6 +105,7 @@ public final class PlayQueueActivity extends AppCompatActivity
|
|||
this.menu = m;
|
||||
getMenuInflater().inflate(R.menu.menu_play_queue, m);
|
||||
getMenuInflater().inflate(R.menu.menu_play_queue_bg, m);
|
||||
buildAudioTrackMenu();
|
||||
onMaybeMuteChanged();
|
||||
// to avoid null reference
|
||||
if (player != null) {
|
||||
|
@ -153,6 +162,12 @@ public final class PlayQueueActivity extends AppCompatActivity
|
|||
NavigationHelper.playOnBackgroundPlayer(this, player.getPlayQueue(), true);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (item.getGroupId() == MENU_ID_AUDIO_TRACK) {
|
||||
onAudioTrackClick(item.getItemId());
|
||||
return true;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -86,6 +86,7 @@ import org.schabi.newpipe.databinding.PlayerBinding;
|
|||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
|
@ -179,13 +180,18 @@ public final class Player implements PlaybackListener, Listener {
|
|||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
// 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 private MediaItemTag currentMetadata;
|
||||
@Nullable private Bitmap currentThumbnail;
|
||||
@Nullable
|
||||
private PlayQueueItem currentItem;
|
||||
@Nullable
|
||||
private MediaItemTag currentMetadata;
|
||||
@Nullable
|
||||
private Bitmap currentThumbnail;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Player
|
||||
|
@ -194,12 +200,17 @@ public final class Player implements PlaybackListener, Listener {
|
|||
private ExoPlayer simpleExoPlayer;
|
||||
private AudioReactor audioReactor;
|
||||
|
||||
@NonNull private final DefaultTrackSelector trackSelector;
|
||||
@NonNull private final LoadController loadController;
|
||||
@NonNull private final DefaultRenderersFactory renderFactory;
|
||||
@NonNull
|
||||
private final DefaultTrackSelector trackSelector;
|
||||
@NonNull
|
||||
private final LoadController loadController;
|
||||
@NonNull
|
||||
private final DefaultRenderersFactory renderFactory;
|
||||
|
||||
@NonNull private final VideoPlaybackResolver videoResolver;
|
||||
@NonNull private final AudioPlaybackResolver audioResolver;
|
||||
@NonNull
|
||||
private final VideoPlaybackResolver videoResolver;
|
||||
@NonNull
|
||||
private final AudioPlaybackResolver audioResolver;
|
||||
|
||||
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 IntentFilter intentFilter;
|
||||
@Nullable private PlayerServiceEventListener fragmentListener = null;
|
||||
@Nullable private PlayerEventListener activityListener = null;
|
||||
@Nullable
|
||||
private PlayerServiceEventListener fragmentListener = null;
|
||||
@Nullable
|
||||
private PlayerEventListener activityListener = null;
|
||||
|
||||
@NonNull private final SerialDisposable progressUpdateDisposable = new SerialDisposable();
|
||||
@NonNull private final CompositeDisposable databaseUpdateDisposable = new CompositeDisposable();
|
||||
@NonNull
|
||||
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
|
||||
// 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.
|
||||
@NonNull private final Target currentThumbnailTarget;
|
||||
@NonNull
|
||||
private final Target currentThumbnailTarget;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@NonNull private final Context context;
|
||||
@NonNull private final SharedPreferences prefs;
|
||||
@NonNull private final HistoryRecordManager recordManager;
|
||||
@NonNull
|
||||
private final Context context;
|
||||
@NonNull
|
||||
private final SharedPreferences prefs;
|
||||
@NonNull
|
||||
private final HistoryRecordManager recordManager;
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
|
@ -333,7 +352,7 @@ public final class Player implements PlaybackListener, Listener {
|
|||
isAudioOnly = audioPlayerSelected();
|
||||
|
||||
if (intent.hasExtra(PLAYBACK_QUALITY)) {
|
||||
setPlaybackQuality(intent.getStringExtra(PLAYBACK_QUALITY));
|
||||
videoResolver.setPlaybackQuality(intent.getStringExtra(PLAYBACK_QUALITY));
|
||||
}
|
||||
|
||||
// Resolve enqueue intents
|
||||
|
@ -341,7 +360,7 @@ public final class Player implements PlaybackListener, Listener {
|
|||
playQueue.append(newQueue.getStreams());
|
||||
return;
|
||||
|
||||
// Resolve enqueue next intents
|
||||
// Resolve enqueue next intents
|
||||
} else if (intent.getBooleanExtra(ENQUEUE_NEXT, false) && playQueue != null) {
|
||||
final int currentIndex = playQueue.getIndex();
|
||||
playQueue.append(newQueue.getStreams());
|
||||
|
@ -913,7 +932,7 @@ public final class Player implements PlaybackListener, Listener {
|
|||
|
||||
private Disposable getProgressUpdateDisposable() {
|
||||
return Observable.interval(PROGRESS_LOOP_INTERVAL_MILLIS, MILLISECONDS,
|
||||
AndroidSchedulers.mainThread())
|
||||
AndroidSchedulers.mainThread())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(ignored -> triggerProgressUpdate(),
|
||||
error -> Log.e(TAG, "Progress update failure: ", error));
|
||||
|
@ -922,7 +941,6 @@ public final class Player implements PlaybackListener, Listener {
|
|||
//endregion
|
||||
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Playback states
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
@ -1244,6 +1262,9 @@ public final class Player implements PlaybackListener, Listener {
|
|||
}
|
||||
final StreamInfo previousInfo = Optional.ofNullable(currentMetadata)
|
||||
.flatMap(MediaItemTag::getMaybeStreamInfo).orElse(null);
|
||||
final MediaItemTag.AudioTrack previousAudioTrack =
|
||||
Optional.ofNullable(currentMetadata)
|
||||
.flatMap(MediaItemTag::getMaybeAudioTrack).orElse(null);
|
||||
currentMetadata = tag;
|
||||
|
||||
if (!currentMetadata.getErrors().isEmpty()) {
|
||||
|
@ -1264,6 +1285,12 @@ public final class Player implements PlaybackListener, Listener {
|
|||
if (previousInfo == null || !previousInfo.getUrl().equals(info.getUrl())) {
|
||||
// only update with the new stream info if it has actually changed
|
||||
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
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
//region Errors
|
||||
|
||||
/**
|
||||
* Process exceptions produced by {@link com.google.android.exoplayer2.ExoPlayer ExoPlayer}.
|
||||
* <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
|
||||
* create a notification so users are aware.
|
||||
* </ul>
|
||||
*
|
||||
* @see com.google.android.exoplayer2.Player.Listener#onPlayerError(PlaybackException)
|
||||
* */
|
||||
*/
|
||||
// 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
|
||||
// shutdown.
|
||||
|
@ -1760,6 +1789,7 @@ public final class Player implements PlaybackListener, Listener {
|
|||
registerStreamViewed();
|
||||
|
||||
notifyMetadataUpdateToListeners();
|
||||
notifyAudioTrackUpdateToListeners();
|
||||
UIs.call(playerUi -> playerUi.onMetadataChanged(info));
|
||||
}
|
||||
|
||||
|
@ -1888,6 +1918,12 @@ public final class Player implements PlaybackListener, Listener {
|
|||
.map(quality -> quality.getSortedVideoStreams()
|
||||
.get(quality.getSelectedVideoStreamIndex()));
|
||||
}
|
||||
|
||||
public Optional<AudioStream> getSelectedAudioStream() {
|
||||
return Optional.ofNullable(currentMetadata)
|
||||
.flatMap(MediaItemTag::getMaybeAudioTrack)
|
||||
.map(MediaItemTag.AudioTrack::getSelectedAudioStream);
|
||||
}
|
||||
//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) {
|
||||
if (playQueue == null || isAudioOnly == !videoEnabled || audioPlayerSelected()) {
|
||||
return;
|
||||
|
@ -2115,7 +2160,7 @@ public final class Player implements PlaybackListener, Listener {
|
|||
// because the stream source will be probably the same as the current played
|
||||
if (sourceType == SourceType.VIDEO_WITH_SEPARATED_AUDIO
|
||||
|| (sourceType == SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY
|
||||
&& isNullOrEmpty(streamInfo.getAudioStreams()))) {
|
||||
&& isNullOrEmpty(streamInfo.getAudioStreams()))) {
|
||||
// It's not needed to reload the play queue manager only if the content's stream type
|
||||
// is a video stream, a live stream or an ended live stream
|
||||
return !StreamTypeUtil.isVideo(streamType);
|
||||
|
@ -2177,7 +2222,18 @@ public final class Player implements PlaybackListener, Listener {
|
|||
}
|
||||
|
||||
public void setPlaybackQuality(@Nullable final String quality) {
|
||||
saveStreamProgressState();
|
||||
setRecovery();
|
||||
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.
|
||||
*
|
||||
* <p>
|
||||
* This method returns the video renderer index of 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.
|
||||
|
|
|
@ -11,5 +11,6 @@ public interface PlayerEventListener {
|
|||
PlaybackParameters parameters);
|
||||
void onProgressUpdate(int currentProgress, int duration, int bufferPercent);
|
||||
void onMetadataUpdate(StreamInfo info, PlayQueue queue);
|
||||
default void onAudioTrackUpdate() { }
|
||||
void onServiceStopped();
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import com.google.android.exoplayer2.MediaItem.RequestMetadata;
|
|||
import com.google.android.exoplayer2.MediaMetadata;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
|
@ -55,6 +56,11 @@ public interface MediaItemTag {
|
|||
return Optional.empty();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
default Optional<AudioTrack> getMaybeAudioTrack() {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
<T> Optional<T> getMaybeExtras(@NonNull Class<T> type);
|
||||
|
||||
<T> MediaItemTag withExtras(@NonNull T extra);
|
||||
|
@ -128,4 +134,37 @@ public interface MediaItemTag {
|
|||
? 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package org.schabi.newpipe.player.mediaitem;
|
|||
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
|
@ -25,25 +26,41 @@ public final class StreamInfoTag implements MediaItemTag {
|
|||
@Nullable
|
||||
private final MediaItemTag.Quality quality;
|
||||
@Nullable
|
||||
private final MediaItemTag.AudioTrack audioTrack;
|
||||
@Nullable
|
||||
private final Object extras;
|
||||
|
||||
private StreamInfoTag(@NonNull final StreamInfo streamInfo,
|
||||
@Nullable final MediaItemTag.Quality quality,
|
||||
@Nullable final MediaItemTag.AudioTrack audioTrack,
|
||||
@Nullable final Object extras) {
|
||||
this.streamInfo = streamInfo;
|
||||
this.quality = quality;
|
||||
this.audioTrack = audioTrack;
|
||||
this.extras = extras;
|
||||
}
|
||||
|
||||
public static StreamInfoTag of(@NonNull final StreamInfo streamInfo,
|
||||
@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);
|
||||
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) {
|
||||
return new StreamInfoTag(streamInfo, null, null);
|
||||
return new StreamInfoTag(streamInfo, null, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -103,6 +120,12 @@ public final class StreamInfoTag implements MediaItemTag {
|
|||
return Optional.ofNullable(quality);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Optional<AudioTrack> getMaybeAudioTrack() {
|
||||
return Optional.ofNullable(audioTrack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Optional<T> getMaybeExtras(@NonNull final Class<T> type) {
|
||||
return Optional.ofNullable(extras).map(type::cast);
|
||||
|
@ -110,6 +133,6 @@ public final class StreamInfoTag implements MediaItemTag {
|
|||
|
||||
@Override
|
||||
public StreamInfoTag withExtras(@NonNull final Object extra) {
|
||||
return new StreamInfoTag(streamInfo, quality, extra);
|
||||
return new StreamInfoTag(streamInfo, quality, audioTrack, extra);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package org.schabi.newpipe.player.resolver;
|
||||
|
||||
import static org.schabi.newpipe.util.ListHelper.getFilteredAudioStreams;
|
||||
import static org.schabi.newpipe.util.ListHelper.getPlayableStreams;
|
||||
|
||||
import android.content.Context;
|
||||
|
@ -28,6 +29,8 @@ public class AudioPlaybackResolver implements PlaybackResolver {
|
|||
private final Context context;
|
||||
@NonNull
|
||||
private final PlayerDataSource dataSource;
|
||||
@Nullable
|
||||
private String audioTrack;
|
||||
|
||||
public AudioPlaybackResolver(@NonNull final Context context,
|
||||
@NonNull final PlayerDataSource dataSource) {
|
||||
|
@ -35,6 +38,13 @@ public class AudioPlaybackResolver implements PlaybackResolver {
|
|||
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
|
||||
@Nullable
|
||||
public MediaSource resolve(@NonNull final StreamInfo info) {
|
||||
|
@ -43,12 +53,27 @@ public class AudioPlaybackResolver implements PlaybackResolver {
|
|||
return liveSource;
|
||||
}
|
||||
|
||||
final Stream stream = getAudioSource(info);
|
||||
if (stream == null) {
|
||||
return null;
|
||||
}
|
||||
final List<AudioStream> audioStreams =
|
||||
getFilteredAudioStreams(context, info.getAudioStreams());
|
||||
final Stream stream;
|
||||
final MediaItemTag tag;
|
||||
|
||||
final MediaItemTag tag = StreamInfoTag.of(info);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
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
|
||||
Stream getStreamForIndex(final int index, @NonNull final List<? extends Stream> streams) {
|
||||
if (index >= 0 && index < streams.size()) {
|
||||
|
@ -91,4 +91,13 @@ public class AudioPlaybackResolver implements PlaybackResolver {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getAudioTrack() {
|
||||
return audioTrack;
|
||||
}
|
||||
|
||||
public void setAudioTrack(@Nullable final String audioLanguage) {
|
||||
this.audioTrack = audioLanguage;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -156,6 +156,16 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
|
|||
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();
|
||||
}
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ import java.util.List;
|
|||
import java.util.Optional;
|
||||
|
||||
import static com.google.android.exoplayer2.C.TIME_UNSET;
|
||||
import static org.schabi.newpipe.util.ListHelper.getFilteredAudioStreams;
|
||||
import static org.schabi.newpipe.util.ListHelper.getUrlAndNonTorrentStreams;
|
||||
import static org.schabi.newpipe.util.ListHelper.getPlayableStreams;
|
||||
|
||||
|
@ -44,6 +45,8 @@ public class VideoPlaybackResolver implements PlaybackResolver {
|
|||
|
||||
@Nullable
|
||||
private String playbackQuality;
|
||||
@Nullable
|
||||
private String audioTrack;
|
||||
|
||||
public enum SourceType {
|
||||
LIVE_STREAM,
|
||||
|
@ -74,19 +77,29 @@ public class VideoPlaybackResolver implements PlaybackResolver {
|
|||
final List<VideoStream> videoStreamsList = ListHelper.getSortedStreamVideosList(context,
|
||||
getPlayableStreams(info.getVideoStreams(), info.getServiceId()),
|
||||
getPlayableStreams(info.getVideoOnlyStreams(), info.getServiceId()), false, true);
|
||||
final int index;
|
||||
final List<AudioStream> audioStreamsList =
|
||||
getFilteredAudioStreams(context, info.getAudioStreams());
|
||||
|
||||
final int videoIndex;
|
||||
if (videoStreamsList.isEmpty()) {
|
||||
index = -1;
|
||||
videoIndex = -1;
|
||||
} else if (playbackQuality == null) {
|
||||
index = qualityResolver.getDefaultResolutionIndex(videoStreamsList);
|
||||
videoIndex = qualityResolver.getDefaultResolutionIndex(videoStreamsList);
|
||||
} else {
|
||||
index = qualityResolver.getOverrideResolutionIndex(videoStreamsList,
|
||||
videoIndex = qualityResolver.getOverrideResolutionIndex(videoStreamsList,
|
||||
getPlaybackQuality());
|
||||
}
|
||||
final MediaItemTag tag = StreamInfoTag.of(info, videoStreamsList, index);
|
||||
|
||||
final int audioIndex =
|
||||
ListHelper.getAudioFormatIndex(context, audioStreamsList, audioTrack);
|
||||
final MediaItemTag tag =
|
||||
StreamInfoTag.of(info, videoStreamsList, videoIndex, audioStreamsList, audioIndex);
|
||||
@Nullable final VideoStream video = tag.getMaybeQuality()
|
||||
.map(MediaItemTag.Quality::getSelectedVideoStream)
|
||||
.orElse(null);
|
||||
@Nullable final AudioStream audio = tag.getMaybeAudioTrack()
|
||||
.map(MediaItemTag.AudioTrack::getSelectedAudioStream)
|
||||
.orElse(null);
|
||||
|
||||
if (video != null) {
|
||||
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
|
||||
// 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 {
|
||||
final MediaSource audioSource = PlaybackResolver.buildMediaSource(
|
||||
dataSource, audio, info, PlaybackResolver.cacheKeyOf(info, audio), tag);
|
||||
|
@ -180,6 +187,15 @@ public class VideoPlaybackResolver implements PlaybackResolver {
|
|||
this.playbackQuality = playbackQuality;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getAudioTrack() {
|
||||
return audioTrack;
|
||||
}
|
||||
|
||||
public void setAudioTrack(@Nullable final String audioLanguage) {
|
||||
this.audioTrack = audioLanguage;
|
||||
}
|
||||
|
||||
public interface QualityResolver {
|
||||
int getDefaultResolutionIndex(List<VideoStream> sortedVideos);
|
||||
|
||||
|
|
|
@ -63,6 +63,7 @@ import org.schabi.newpipe.App;
|
|||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.PlayerBinding;
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
||||
|
@ -78,6 +79,7 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
|||
import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper;
|
||||
import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHolder;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
|
@ -108,7 +110,8 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
|||
|
||||
protected PlayerBinding binding;
|
||||
private final Handler controlsVisibilityHandler = new Handler(Looper.getMainLooper());
|
||||
@Nullable private SurfaceHolderCallback surfaceHolderCallback;
|
||||
@Nullable
|
||||
private SurfaceHolderCallback surfaceHolderCallback;
|
||||
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_AUDIO_TRACK = 70;
|
||||
private static final int POPUP_MENU_ID_PLAYBACK_SPEED = 79;
|
||||
private static final int POPUP_MENU_ID_CAPTION = 89;
|
||||
|
||||
protected boolean isSomePopupMenuVisible = false;
|
||||
private PopupMenu qualityPopupMenu;
|
||||
private PopupMenu audioTrackPopupMenu;
|
||||
protected PopupMenu playbackSpeedPopupMenu;
|
||||
private PopupMenu captionPopupMenu;
|
||||
|
||||
|
@ -146,7 +151,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
|||
//region Constructor, setup, destroy
|
||||
|
||||
protected VideoPlayerUi(@NonNull final Player player,
|
||||
@NonNull final PlayerBinding playerBinding) {
|
||||
@NonNull final PlayerBinding playerBinding) {
|
||||
super(player);
|
||||
binding = playerBinding;
|
||||
setupFromView();
|
||||
|
@ -173,6 +178,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
|||
R.style.DarkPopupMenu);
|
||||
|
||||
qualityPopupMenu = new PopupMenu(themeWrapper, binding.qualityTextView);
|
||||
audioTrackPopupMenu = new PopupMenu(themeWrapper, binding.audioTrackTextView);
|
||||
playbackSpeedPopupMenu = new PopupMenu(context, binding.playbackSpeed);
|
||||
captionPopupMenu = new PopupMenu(themeWrapper, binding.captionTextView);
|
||||
|
||||
|
@ -190,6 +196,8 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
|||
|
||||
protected void initListeners() {
|
||||
binding.qualityTextView.setOnClickListener(makeOnClickListener(this::onQualityClicked));
|
||||
binding.audioTrackTextView.setOnClickListener(
|
||||
makeOnClickListener(this::onAudioTracksClicked));
|
||||
binding.playbackSpeed.setOnClickListener(makeOnClickListener(this::onPlaybackSpeedClicked));
|
||||
|
||||
binding.playbackSeekBar.setOnSeekBarChangeListener(this);
|
||||
|
@ -266,6 +274,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
|||
|
||||
protected void deinitListeners() {
|
||||
binding.qualityTextView.setOnClickListener(null);
|
||||
binding.audioTrackTextView.setOnClickListener(null);
|
||||
binding.playbackSpeed.setOnClickListener(null);
|
||||
binding.playbackSeekBar.setOnSeekBarChangeListener(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.bottomControls.setPaddingRelative(controlsPad, 0, controlsPad, 0);
|
||||
binding.qualityTextView.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad);
|
||||
binding.audioTrackTextView.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad);
|
||||
binding.playbackSpeed.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad);
|
||||
binding.playbackSpeed.setMinimumWidth(buttonsMinWidth);
|
||||
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.
|
||||
*
|
||||
* @param currentProgress the current progress, in milliseconds
|
||||
*/
|
||||
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).
|
||||
*
|
||||
* @param duration the video duration, in milliseconds
|
||||
*/
|
||||
private void setVideoDurationToControls(final int duration) {
|
||||
|
@ -984,6 +996,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
|||
private void updateStreamRelatedViews() {
|
||||
player.getCurrentStreamInfo().ifPresent(info -> {
|
||||
binding.qualityTextView.setVisibility(View.GONE);
|
||||
binding.audioTrackTextView.setVisibility(View.GONE);
|
||||
binding.playbackSpeed.setVisibility(View.GONE);
|
||||
|
||||
binding.playbackEndTime.setVisibility(View.GONE);
|
||||
|
@ -1019,6 +1032,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
|||
}
|
||||
|
||||
buildQualityMenu();
|
||||
buildAudioTrackMenu();
|
||||
|
||||
binding.qualityTextView.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()));
|
||||
}
|
||||
|
||||
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() {
|
||||
if (playbackSpeedPopupMenu == null) {
|
||||
return;
|
||||
|
@ -1175,6 +1217,11 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
|||
.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.
|
||||
*/
|
||||
|
@ -1187,26 +1234,10 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
|||
}
|
||||
|
||||
if (menuItem.getGroupId() == POPUP_MENU_ID_QUALITY) {
|
||||
final int menuItemIndex = menuItem.getItemId();
|
||||
@Nullable final MediaItemTag currentMetadata = player.getCurrentMetadata();
|
||||
if (currentMetadata == null || currentMetadata.getMaybeQuality().isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
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());
|
||||
onQualityItemClick(menuItem);
|
||||
return true;
|
||||
} else if (menuItem.getGroupId() == POPUP_MENU_ID_AUDIO_TRACK) {
|
||||
onAudioTrackItemClick(menuItem);
|
||||
return true;
|
||||
} else if (menuItem.getGroupId() == POPUP_MENU_ID_PLAYBACK_SPEED) {
|
||||
final int speedIndex = menuItem.getItemId();
|
||||
|
@ -1219,6 +1250,47 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
|||
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.
|
||||
*/
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,6 +15,7 @@ import androidx.preference.PreferenceManager;
|
|||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
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.Stream;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
|
@ -25,6 +26,7 @@ import java.util.Collections;
|
|||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
|
@ -38,11 +40,17 @@ public final class ListHelper {
|
|||
// Audio format in order of quality. 0=lowest quality, n=highest quality
|
||||
private static final List<MediaFormat> AUDIO_FORMAT_QUALITY_RANKING =
|
||||
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 =
|
||||
List.of(MediaFormat.WEBMA, MediaFormat.M4A, MediaFormat.MP3);
|
||||
List.of(MediaFormat.MP3, MediaFormat.M4A, MediaFormat.WEBMA);
|
||||
// Use a Set for better performance
|
||||
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.
|
||||
|
@ -62,10 +70,10 @@ public final class ListHelper {
|
|||
private ListHelper() { }
|
||||
|
||||
/**
|
||||
* @see #getDefaultResolutionIndex(String, String, MediaFormat, List)
|
||||
* @param context Android app context
|
||||
* @param videoStreams list of the video streams to check
|
||||
* @return index of the video stream with the default index
|
||||
* @see #getDefaultResolutionIndex(String, String, MediaFormat, List)
|
||||
*/
|
||||
public static int getDefaultResolutionIndex(final Context context,
|
||||
final List<VideoStream> videoStreams) {
|
||||
|
@ -75,11 +83,11 @@ public final class ListHelper {
|
|||
}
|
||||
|
||||
/**
|
||||
* @see #getDefaultResolutionIndex(String, String, MediaFormat, List)
|
||||
* @param context Android app context
|
||||
* @param videoStreams list of the video streams to check
|
||||
* @param defaultResolution the default resolution to look for
|
||||
* @return index of the video stream with the default index
|
||||
* @see #getDefaultResolutionIndex(String, String, MediaFormat, List)
|
||||
*/
|
||||
public static int getResolutionIndex(final Context context,
|
||||
final List<VideoStream> videoStreams,
|
||||
|
@ -88,10 +96,10 @@ public final class ListHelper {
|
|||
}
|
||||
|
||||
/**
|
||||
* @see #getDefaultResolutionIndex(String, String, MediaFormat, List)
|
||||
* @param context Android app context
|
||||
* @param videoStreams list of the video streams to check
|
||||
* @param context Android app context
|
||||
* @param videoStreams list of the video streams to check
|
||||
* @return index of the video stream with the default index
|
||||
* @see #getDefaultResolutionIndex(String, String, MediaFormat, List)
|
||||
*/
|
||||
public static int getPopupDefaultResolutionIndex(final Context context,
|
||||
final List<VideoStream> videoStreams) {
|
||||
|
@ -101,11 +109,11 @@ public final class ListHelper {
|
|||
}
|
||||
|
||||
/**
|
||||
* @see #getDefaultResolutionIndex(String, String, MediaFormat, List)
|
||||
* @param context Android app context
|
||||
* @param videoStreams list of the video streams to check
|
||||
* @param defaultResolution the default resolution to look for
|
||||
* @return index of the video stream with the default index
|
||||
* @see #getDefaultResolutionIndex(String, String, MediaFormat, List)
|
||||
*/
|
||||
public static int getPopupResolutionIndex(final Context context,
|
||||
final List<VideoStream> videoStreams,
|
||||
|
@ -115,16 +123,36 @@ public final class ListHelper {
|
|||
|
||||
public static int getDefaultAudioFormat(final Context context,
|
||||
final List<AudioStream> audioStreams) {
|
||||
final MediaFormat defaultFormat = getDefaultFormat(context,
|
||||
R.string.default_audio_format_key, R.string.default_audio_format_value);
|
||||
return getAudioIndexByHighestRank(audioStreams,
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
@ -325,8 +437,8 @@ public final class ListHelper {
|
|||
// Filter out higher resolutions (or not if high resolutions should always be shown)
|
||||
.filter(stream -> showHigherResolutions
|
||||
|| !HIGH_RESOLUTION_LIST.contains(stream.getResolution()
|
||||
// Replace any frame rate with nothing
|
||||
.replaceAll("p\\d+$", "p")))
|
||||
// Replace any frame rate with nothing
|
||||
.replaceAll("p\\d+$", "p")))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
final HashMap<String, VideoStream> hashMap = new HashMap<>();
|
||||
|
@ -376,72 +488,22 @@ public final class ListHelper {
|
|||
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.
|
||||
* 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 comparator The comparator used for determining the max/best/highest ranked value
|
||||
* @param audioStreams List of audio streams
|
||||
* @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
|
||||
*/
|
||||
private static int getAudioIndexByHighestRank(@Nullable final MediaFormat targetedFormat,
|
||||
@Nullable final List<AudioStream> audioStreams,
|
||||
final Comparator<AudioStream> comparator) {
|
||||
static int getAudioIndexByHighestRank(@Nullable final List<AudioStream> audioStreams,
|
||||
final Comparator<AudioStream> comparator) {
|
||||
if (audioStreams == null || audioStreams.isEmpty()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
final AudioStream highestRankedAudioStream = audioStreams.stream()
|
||||
.filter(audioStream -> targetedFormat == 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;
|
||||
}
|
||||
.max(comparator).orElse(null);
|
||||
|
||||
return audioStreams.indexOf(highestRankedAudioStream);
|
||||
}
|
||||
|
@ -629,4 +691,149 @@ public final class ListHelper {
|
|||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import android.text.TextUtils;
|
|||
import android.util.DisplayMetrics;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.PluralsRes;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.math.MathUtils;
|
||||
|
@ -21,6 +22,8 @@ import org.ocpsoft.prettytime.units.Decade;
|
|||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
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.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
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
|
|
@ -224,6 +224,8 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
|
|||
public static class StreamSizeWrapper<T extends Stream> implements Serializable {
|
||||
private static final StreamSizeWrapper<Stream> EMPTY =
|
||||
new StreamSizeWrapper<>(Collections.emptyList(), null);
|
||||
private static final int SIZE_UNSET = -2;
|
||||
|
||||
private final List<T> streamsList;
|
||||
private final long[] streamSizes;
|
||||
private final String unknownSize;
|
||||
|
@ -235,7 +237,7 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
|
|||
this.unknownSize = context == null
|
||||
? "--.-" : 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 = () -> {
|
||||
boolean hasChanged = false;
|
||||
for (final X stream : streamsWrapper.getStreamsList()) {
|
||||
if (streamsWrapper.getSizeInBytes(stream) > -2) {
|
||||
if (streamsWrapper.getSizeInBytes(stream) > SIZE_UNSET) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -269,6 +271,10 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
|
|||
.onErrorReturnItem(true);
|
||||
}
|
||||
|
||||
public void resetSizes() {
|
||||
Arrays.fill(streamSizes, SIZE_UNSET);
|
||||
}
|
||||
|
||||
public static <X extends Stream> StreamSizeWrapper<X> empty() {
|
||||
//noinspection unchecked
|
||||
return (StreamSizeWrapper<X>) EMPTY;
|
||||
|
|
|
@ -71,11 +71,45 @@
|
|||
android:minWidth="150dp"
|
||||
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
|
||||
android:id="@+id/threads_text_view"
|
||||
android:layout_width="match_parent"
|
||||
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_marginRight="24dp"
|
||||
android:layout_marginBottom="6dp"
|
||||
|
|
|
@ -157,6 +157,22 @@
|
|||
tools:text="The Video Artist LONG very LONG very Long" />
|
||||
</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
|
||||
android:id="@+id/qualityTextView"
|
||||
android:layout_width="wrap_content"
|
||||
|
|
|
@ -18,6 +18,14 @@
|
|||
android:visible="true"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_audio_track"
|
||||
android:tooltipText="@string/audio_track"
|
||||
android:visible="false"
|
||||
app:showAsAction="ifRoom">
|
||||
<menu />
|
||||
</item>
|
||||
|
||||
<item
|
||||
android:id="@+id/action_mute"
|
||||
android:icon="@drawable/ic_volume_off"
|
||||
|
|
|
@ -219,6 +219,8 @@
|
|||
<item>@string/none_control_key</item>
|
||||
</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>
|
||||
|
||||
<!-- DEBUG ONLY -->
|
||||
|
|
|
@ -94,6 +94,10 @@
|
|||
<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_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="metadata_cache_wipe_title">Wipe cached metadata</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_stream_detail">Details</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="show_channel_details">Show channel details</string>
|
||||
<string name="enqueue_stream">Enqueue</string>
|
||||
|
@ -761,12 +767,15 @@
|
|||
<string name="enumeration_comma">,</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="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="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="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_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_hide_future_items">Hide future items</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="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="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>
|
|
@ -61,6 +61,22 @@
|
|||
app:iconSpaceReserved="false"
|
||||
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
|
||||
android:fragment="org.schabi.newpipe.settings.ExoPlayerSettingsFragment"
|
||||
android:key="@string/exoplayer_settings_key"
|
||||
|
|
|
@ -3,10 +3,13 @@ package org.schabi.newpipe.util;
|
|||
import org.junit.Test;
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream.AudioTrackType;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
|
@ -29,6 +32,15 @@ public class ListHelperTest {
|
|||
generateAudioStream("mp3-192", MediaFormat.MP3, 192),
|
||||
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(
|
||||
generateVideoStream("mpeg_4-720", MediaFormat.MPEG_4, "720p", false),
|
||||
generateVideoStream("v3gpp-240", MediaFormat.v3GPP, "240p", false),
|
||||
|
@ -199,24 +211,29 @@ public class ListHelperTest {
|
|||
|
||||
@Test
|
||||
public void getHighestQualityAudioFormatTest() {
|
||||
AudioStream stream = AUDIO_STREAMS_TEST_LIST.get(ListHelper.getHighestQualityAudioIndex(
|
||||
MediaFormat.M4A, AUDIO_STREAMS_TEST_LIST));
|
||||
Comparator<AudioStream> cmp = ListHelper.getAudioFormatComparator(MediaFormat.M4A, false);
|
||||
AudioStream stream = AUDIO_STREAMS_TEST_LIST.get(ListHelper.getAudioIndexByHighestRank(
|
||||
AUDIO_STREAMS_TEST_LIST, cmp));
|
||||
assertEquals(320, stream.getAverageBitrate());
|
||||
assertEquals(MediaFormat.M4A, stream.getFormat());
|
||||
|
||||
stream = AUDIO_STREAMS_TEST_LIST.get(ListHelper.getHighestQualityAudioIndex(
|
||||
MediaFormat.WEBMA, AUDIO_STREAMS_TEST_LIST));
|
||||
cmp = ListHelper.getAudioFormatComparator(MediaFormat.WEBMA, false);
|
||||
stream = AUDIO_STREAMS_TEST_LIST.get(ListHelper.getAudioIndexByHighestRank(
|
||||
AUDIO_STREAMS_TEST_LIST, cmp));
|
||||
assertEquals(320, stream.getAverageBitrate());
|
||||
assertEquals(MediaFormat.WEBMA, stream.getFormat());
|
||||
|
||||
stream = AUDIO_STREAMS_TEST_LIST.get(ListHelper.getHighestQualityAudioIndex(
|
||||
MediaFormat.MP3, AUDIO_STREAMS_TEST_LIST));
|
||||
cmp = ListHelper.getAudioFormatComparator(MediaFormat.MP3, false);
|
||||
stream = AUDIO_STREAMS_TEST_LIST.get(ListHelper.getAudioIndexByHighestRank(
|
||||
AUDIO_STREAMS_TEST_LIST, cmp));
|
||||
assertEquals(192, stream.getAverageBitrate());
|
||||
assertEquals(MediaFormat.MP3, stream.getFormat());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getHighestQualityAudioFormatPreferredAbsent() {
|
||||
final Comparator<AudioStream> cmp =
|
||||
ListHelper.getAudioFormatComparator(MediaFormat.MP3, false);
|
||||
|
||||
//////////////////////////////////////////
|
||||
// Doesn't contain the preferred format //
|
||||
|
@ -227,8 +244,7 @@ public class ListHelperTest {
|
|||
generateAudioStream("webma-192", MediaFormat.WEBMA, 192));
|
||||
// List doesn't contains this format
|
||||
// It should fallback to the highest bitrate audio no matter what format it is
|
||||
AudioStream stream = testList.get(ListHelper.getHighestQualityAudioIndex(
|
||||
MediaFormat.MP3, testList));
|
||||
AudioStream stream = testList.get(ListHelper.getAudioIndexByHighestRank(testList, cmp));
|
||||
assertEquals(192, stream.getAverageBitrate());
|
||||
assertEquals(MediaFormat.WEBMA, stream.getFormat());
|
||||
|
||||
|
@ -246,44 +262,51 @@ public class ListHelperTest {
|
|||
generateAudioStream("webma-192-4", MediaFormat.WEBMA, 192)));
|
||||
// List doesn't contains this format, it should fallback to the highest bitrate audio and
|
||||
// the highest quality format.
|
||||
stream = testList.get(ListHelper.getHighestQualityAudioIndex(MediaFormat.MP3, testList));
|
||||
stream =
|
||||
testList.get(ListHelper.getAudioIndexByHighestRank(testList, cmp));
|
||||
assertEquals(192, stream.getAverageBitrate());
|
||||
assertEquals(MediaFormat.M4A, stream.getFormat());
|
||||
|
||||
// Adding a new format and bitrate. Adding another stream will have no impact since
|
||||
// it's not a preferred format.
|
||||
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(MediaFormat.M4A, stream.getFormat());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getHighestQualityAudioNull() {
|
||||
assertEquals(-1, ListHelper.getHighestQualityAudioIndex(null, null));
|
||||
assertEquals(-1, ListHelper.getHighestQualityAudioIndex(null, new ArrayList<>()));
|
||||
final Comparator<AudioStream> cmp = ListHelper.getAudioFormatComparator(null, false);
|
||||
assertEquals(-1, ListHelper.getAudioIndexByHighestRank(null, cmp));
|
||||
assertEquals(-1, ListHelper.getAudioIndexByHighestRank(new ArrayList<>(), cmp));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getLowestQualityAudioFormatTest() {
|
||||
AudioStream stream = AUDIO_STREAMS_TEST_LIST.get(ListHelper.getMostCompactAudioIndex(
|
||||
MediaFormat.M4A, AUDIO_STREAMS_TEST_LIST));
|
||||
Comparator<AudioStream> cmp = ListHelper.getAudioFormatComparator(MediaFormat.M4A, true);
|
||||
AudioStream stream = AUDIO_STREAMS_TEST_LIST.get(ListHelper.getAudioIndexByHighestRank(
|
||||
AUDIO_STREAMS_TEST_LIST, cmp));
|
||||
assertEquals(128, stream.getAverageBitrate());
|
||||
assertEquals(MediaFormat.M4A, stream.getFormat());
|
||||
|
||||
stream = AUDIO_STREAMS_TEST_LIST.get(ListHelper.getMostCompactAudioIndex(
|
||||
MediaFormat.WEBMA, AUDIO_STREAMS_TEST_LIST));
|
||||
cmp = ListHelper.getAudioFormatComparator(MediaFormat.WEBMA, true);
|
||||
stream = AUDIO_STREAMS_TEST_LIST.get(ListHelper.getAudioIndexByHighestRank(
|
||||
AUDIO_STREAMS_TEST_LIST, cmp));
|
||||
assertEquals(64, stream.getAverageBitrate());
|
||||
assertEquals(MediaFormat.WEBMA, stream.getFormat());
|
||||
|
||||
stream = AUDIO_STREAMS_TEST_LIST.get(ListHelper.getMostCompactAudioIndex(
|
||||
MediaFormat.MP3, AUDIO_STREAMS_TEST_LIST));
|
||||
cmp = ListHelper.getAudioFormatComparator(MediaFormat.MP3, true);
|
||||
stream = AUDIO_STREAMS_TEST_LIST.get(ListHelper.getAudioIndexByHighestRank(
|
||||
AUDIO_STREAMS_TEST_LIST, cmp));
|
||||
assertEquals(64, stream.getAverageBitrate());
|
||||
assertEquals(MediaFormat.MP3, stream.getFormat());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getLowestQualityAudioFormatPreferredAbsent() {
|
||||
Comparator<AudioStream> cmp = ListHelper.getAudioFormatComparator(MediaFormat.MP3, true);
|
||||
|
||||
//////////////////////////////////////////
|
||||
// Doesn't contain the preferred format //
|
||||
|
@ -294,14 +317,13 @@ public class ListHelperTest {
|
|||
generateAudioStream("webma-192-1", MediaFormat.WEBMA, 192)));
|
||||
// List doesn't contains this format
|
||||
// It should fallback to the most compact audio no matter what format it is.
|
||||
AudioStream stream = testList.get(ListHelper.getMostCompactAudioIndex(
|
||||
MediaFormat.MP3, testList));
|
||||
AudioStream stream = testList.get(ListHelper.getAudioIndexByHighestRank(testList, cmp));
|
||||
assertEquals(128, stream.getAverageBitrate());
|
||||
assertEquals(MediaFormat.M4A, stream.getFormat());
|
||||
|
||||
// WEBMA is more compact than M4A
|
||||
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(MediaFormat.WEBMA, stream.getFormat());
|
||||
|
||||
|
@ -318,20 +340,58 @@ public class ListHelperTest {
|
|||
generateAudioStream("m4a-192-3", MediaFormat.M4A, 192)));
|
||||
// List doesn't contain this format
|
||||
// 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(MediaFormat.WEBMA, stream.getFormat());
|
||||
|
||||
// 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(MediaFormat.WEBMA, stream.getFormat());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getLowestQualityAudioNull() {
|
||||
assertEquals(-1, ListHelper.getMostCompactAudioIndex(null, null));
|
||||
assertEquals(-1, ListHelper.getMostCompactAudioIndex(null, new ArrayList<>()));
|
||||
final Comparator<AudioStream> cmp = ListHelper.getAudioFormatComparator(null, false);
|
||||
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
|
||||
|
@ -390,6 +450,22 @@ public class ListHelperTest {
|
|||
.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
|
||||
private static VideoStream generateVideoStream(@NonNull final String id,
|
||||
@Nullable final MediaFormat mediaFormat,
|
||||
|
|
Loading…
Reference in New Issue