mirror of
https://github.com/TeamNewPipe/NewPipe.git
synced 2024-11-25 12:32:31 +01:00
Merge pull request #9937 from Theta-Dev/alang-selector
Add support for multiple audio tracks
This commit is contained in:
commit
2315b082ff
@ -68,6 +68,8 @@ import org.schabi.newpipe.util.SecondaryStreamHelper;
|
|||||||
import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener;
|
import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener;
|
||||||
import org.schabi.newpipe.util.StreamItemAdapter;
|
import org.schabi.newpipe.util.StreamItemAdapter;
|
||||||
import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper;
|
import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper;
|
||||||
|
import org.schabi.newpipe.util.AudioTrackAdapter;
|
||||||
|
import org.schabi.newpipe.util.AudioTrackAdapter.AudioTracksWrapper;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@ -95,12 +97,14 @@ public class DownloadDialog extends DialogFragment
|
|||||||
@State
|
@State
|
||||||
StreamInfo currentInfo;
|
StreamInfo currentInfo;
|
||||||
@State
|
@State
|
||||||
StreamSizeWrapper<AudioStream> wrappedAudioStreams;
|
|
||||||
@State
|
|
||||||
StreamSizeWrapper<VideoStream> wrappedVideoStreams;
|
StreamSizeWrapper<VideoStream> wrappedVideoStreams;
|
||||||
@State
|
@State
|
||||||
StreamSizeWrapper<SubtitlesStream> wrappedSubtitleStreams;
|
StreamSizeWrapper<SubtitlesStream> wrappedSubtitleStreams;
|
||||||
@State
|
@State
|
||||||
|
AudioTracksWrapper wrappedAudioTracks;
|
||||||
|
@State
|
||||||
|
int selectedAudioTrackIndex;
|
||||||
|
@State
|
||||||
int selectedVideoIndex; // set in the constructor
|
int selectedVideoIndex; // set in the constructor
|
||||||
@State
|
@State
|
||||||
int selectedAudioIndex = 0; // default to the first item
|
int selectedAudioIndex = 0; // default to the first item
|
||||||
@ -117,6 +121,7 @@ public class DownloadDialog extends DialogFragment
|
|||||||
private Context context;
|
private Context context;
|
||||||
private boolean askForSavePath;
|
private boolean askForSavePath;
|
||||||
|
|
||||||
|
private AudioTrackAdapter audioTrackAdapter;
|
||||||
private StreamItemAdapter<AudioStream, Stream> audioStreamsAdapter;
|
private StreamItemAdapter<AudioStream, Stream> audioStreamsAdapter;
|
||||||
private StreamItemAdapter<VideoStream, AudioStream> videoStreamsAdapter;
|
private StreamItemAdapter<VideoStream, AudioStream> videoStreamsAdapter;
|
||||||
private StreamItemAdapter<SubtitlesStream, Stream> subtitleStreamsAdapter;
|
private StreamItemAdapter<SubtitlesStream, Stream> subtitleStreamsAdapter;
|
||||||
@ -163,18 +168,26 @@ public class DownloadDialog extends DialogFragment
|
|||||||
public DownloadDialog(@NonNull final Context context, @NonNull final StreamInfo info) {
|
public DownloadDialog(@NonNull final Context context, @NonNull final StreamInfo info) {
|
||||||
this.currentInfo = info;
|
this.currentInfo = info;
|
||||||
|
|
||||||
|
final List<AudioStream> audioStreams =
|
||||||
|
getStreamsOfSpecifiedDelivery(info.getAudioStreams(), PROGRESSIVE_HTTP);
|
||||||
|
final List<List<AudioStream>> groupedAudioStreams =
|
||||||
|
ListHelper.getGroupedAudioStreams(context, audioStreams);
|
||||||
|
this.wrappedAudioTracks = new AudioTracksWrapper(groupedAudioStreams, context);
|
||||||
|
this.selectedAudioTrackIndex =
|
||||||
|
ListHelper.getDefaultAudioTrackGroup(context, groupedAudioStreams);
|
||||||
|
|
||||||
// TODO: Adapt this code when the downloader support other types of stream deliveries
|
// TODO: Adapt this code when the downloader support other types of stream deliveries
|
||||||
final List<VideoStream> videoStreams = ListHelper.getSortedStreamVideosList(
|
final List<VideoStream> videoStreams = ListHelper.getSortedStreamVideosList(
|
||||||
context,
|
context,
|
||||||
getStreamsOfSpecifiedDelivery(info.getVideoStreams(), PROGRESSIVE_HTTP),
|
getStreamsOfSpecifiedDelivery(info.getVideoStreams(), PROGRESSIVE_HTTP),
|
||||||
getStreamsOfSpecifiedDelivery(info.getVideoOnlyStreams(), PROGRESSIVE_HTTP),
|
getStreamsOfSpecifiedDelivery(info.getVideoOnlyStreams(), PROGRESSIVE_HTTP),
|
||||||
false,
|
false,
|
||||||
false
|
// If there are multiple languages available, prefer streams without audio
|
||||||
|
// to allow language selection
|
||||||
|
wrappedAudioTracks.size() > 1
|
||||||
);
|
);
|
||||||
|
|
||||||
this.wrappedVideoStreams = new StreamSizeWrapper<>(videoStreams, context);
|
this.wrappedVideoStreams = new StreamSizeWrapper<>(videoStreams, context);
|
||||||
this.wrappedAudioStreams = new StreamSizeWrapper<>(
|
|
||||||
getStreamsOfSpecifiedDelivery(info.getAudioStreams(), PROGRESSIVE_HTTP), context);
|
|
||||||
this.wrappedSubtitleStreams = new StreamSizeWrapper<>(
|
this.wrappedSubtitleStreams = new StreamSizeWrapper<>(
|
||||||
getStreamsOfSpecifiedDelivery(info.getSubtitles(), PROGRESSIVE_HTTP), context);
|
getStreamsOfSpecifiedDelivery(info.getSubtitles(), PROGRESSIVE_HTTP), context);
|
||||||
|
|
||||||
@ -212,33 +225,9 @@ public class DownloadDialog extends DialogFragment
|
|||||||
setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context));
|
setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context));
|
||||||
Icepick.restoreInstanceState(this, savedInstanceState);
|
Icepick.restoreInstanceState(this, savedInstanceState);
|
||||||
|
|
||||||
final var secondaryStreams = new SparseArrayCompat<SecondaryStreamHelper<AudioStream>>(4);
|
this.audioTrackAdapter = new AudioTrackAdapter(wrappedAudioTracks);
|
||||||
final List<VideoStream> videoStreams = wrappedVideoStreams.getStreamsList();
|
|
||||||
|
|
||||||
for (int i = 0; i < videoStreams.size(); i++) {
|
|
||||||
if (!videoStreams.get(i).isVideoOnly()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
final AudioStream audioStream = SecondaryStreamHelper
|
|
||||||
.getAudioStreamFor(wrappedAudioStreams.getStreamsList(), videoStreams.get(i));
|
|
||||||
|
|
||||||
if (audioStream != null) {
|
|
||||||
secondaryStreams.append(i, new SecondaryStreamHelper<>(wrappedAudioStreams,
|
|
||||||
audioStream));
|
|
||||||
} else if (DEBUG) {
|
|
||||||
final MediaFormat mediaFormat = videoStreams.get(i).getFormat();
|
|
||||||
if (mediaFormat != null) {
|
|
||||||
Log.w(TAG, "No audio stream candidates for video format "
|
|
||||||
+ mediaFormat.name());
|
|
||||||
} else {
|
|
||||||
Log.w(TAG, "No audio stream candidates for unknown video format");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.videoStreamsAdapter = new StreamItemAdapter<>(wrappedVideoStreams, secondaryStreams);
|
|
||||||
this.audioStreamsAdapter = new StreamItemAdapter<>(wrappedAudioStreams);
|
|
||||||
this.subtitleStreamsAdapter = new StreamItemAdapter<>(wrappedSubtitleStreams);
|
this.subtitleStreamsAdapter = new StreamItemAdapter<>(wrappedSubtitleStreams);
|
||||||
|
updateSecondaryStreams();
|
||||||
|
|
||||||
final Intent intent = new Intent(context, DownloadManagerService.class);
|
final Intent intent = new Intent(context, DownloadManagerService.class);
|
||||||
context.startService(intent);
|
context.startService(intent);
|
||||||
@ -265,6 +254,39 @@ public class DownloadDialog extends DialogFragment
|
|||||||
}, Context.BIND_AUTO_CREATE);
|
}, Context.BIND_AUTO_CREATE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the displayed video streams based on the selected audio track.
|
||||||
|
*/
|
||||||
|
private void updateSecondaryStreams() {
|
||||||
|
final StreamSizeWrapper<AudioStream> audioStreams = getWrappedAudioStreams();
|
||||||
|
final var secondaryStreams = new SparseArrayCompat<SecondaryStreamHelper<AudioStream>>(4);
|
||||||
|
final List<VideoStream> videoStreams = wrappedVideoStreams.getStreamsList();
|
||||||
|
wrappedVideoStreams.resetSizes();
|
||||||
|
|
||||||
|
for (int i = 0; i < videoStreams.size(); i++) {
|
||||||
|
if (!videoStreams.get(i).isVideoOnly()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final AudioStream audioStream = SecondaryStreamHelper
|
||||||
|
.getAudioStreamFor(audioStreams.getStreamsList(), videoStreams.get(i));
|
||||||
|
|
||||||
|
if (audioStream != null) {
|
||||||
|
secondaryStreams.append(i, new SecondaryStreamHelper<>(audioStreams, audioStream));
|
||||||
|
} else if (DEBUG) {
|
||||||
|
final MediaFormat mediaFormat = videoStreams.get(i).getFormat();
|
||||||
|
if (mediaFormat != null) {
|
||||||
|
Log.w(TAG, "No audio stream candidates for video format "
|
||||||
|
+ mediaFormat.name());
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "No audio stream candidates for unknown video format");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.videoStreamsAdapter = new StreamItemAdapter<>(wrappedVideoStreams, secondaryStreams);
|
||||||
|
this.audioStreamsAdapter = new StreamItemAdapter<>(audioStreams);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(@NonNull final LayoutInflater inflater,
|
public View onCreateView(@NonNull final LayoutInflater inflater,
|
||||||
final ViewGroup container,
|
final ViewGroup container,
|
||||||
@ -285,13 +307,13 @@ public class DownloadDialog extends DialogFragment
|
|||||||
|
|
||||||
dialogBinding.fileName.setText(FilenameUtils.createFilename(getContext(),
|
dialogBinding.fileName.setText(FilenameUtils.createFilename(getContext(),
|
||||||
currentInfo.getName()));
|
currentInfo.getName()));
|
||||||
selectedAudioIndex = ListHelper
|
selectedAudioIndex = ListHelper.getDefaultAudioFormat(getContext(),
|
||||||
.getDefaultAudioFormat(getContext(), wrappedAudioStreams.getStreamsList());
|
getWrappedAudioStreams().getStreamsList());
|
||||||
|
|
||||||
selectedSubtitleIndex = getSubtitleIndexBy(subtitleStreamsAdapter.getAll());
|
selectedSubtitleIndex = getSubtitleIndexBy(subtitleStreamsAdapter.getAll());
|
||||||
|
|
||||||
dialogBinding.qualitySpinner.setOnItemSelectedListener(this);
|
dialogBinding.qualitySpinner.setOnItemSelectedListener(this);
|
||||||
|
dialogBinding.audioTrackSpinner.setOnItemSelectedListener(this);
|
||||||
dialogBinding.videoAudioGroup.setOnCheckedChangeListener(this);
|
dialogBinding.videoAudioGroup.setOnCheckedChangeListener(this);
|
||||||
|
|
||||||
initToolbar(dialogBinding.toolbarLayout.toolbar);
|
initToolbar(dialogBinding.toolbarLayout.toolbar);
|
||||||
@ -383,7 +405,7 @@ public class DownloadDialog extends DialogFragment
|
|||||||
new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
|
new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
|
||||||
"Downloading video stream size",
|
"Downloading video stream size",
|
||||||
currentInfo.getServiceId()))));
|
currentInfo.getServiceId()))));
|
||||||
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedAudioStreams)
|
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(getWrappedAudioStreams())
|
||||||
.subscribe(result -> {
|
.subscribe(result -> {
|
||||||
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()
|
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()
|
||||||
== R.id.audio_button) {
|
== R.id.audio_button) {
|
||||||
@ -405,14 +427,28 @@ public class DownloadDialog extends DialogFragment
|
|||||||
currentInfo.getServiceId()))));
|
currentInfo.getServiceId()))));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setupAudioTrackSpinner() {
|
||||||
|
if (getContext() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dialogBinding.audioTrackSpinner.setAdapter(audioTrackAdapter);
|
||||||
|
dialogBinding.audioTrackSpinner.setSelection(selectedAudioTrackIndex);
|
||||||
|
}
|
||||||
|
|
||||||
private void setupAudioSpinner() {
|
private void setupAudioSpinner() {
|
||||||
if (getContext() == null) {
|
if (getContext() == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dialogBinding.qualitySpinner.setAdapter(audioStreamsAdapter);
|
dialogBinding.qualitySpinner.setVisibility(View.GONE);
|
||||||
dialogBinding.qualitySpinner.setSelection(selectedAudioIndex);
|
|
||||||
setRadioButtonsState(true);
|
setRadioButtonsState(true);
|
||||||
|
dialogBinding.audioStreamSpinner.setAdapter(audioStreamsAdapter);
|
||||||
|
dialogBinding.audioStreamSpinner.setSelection(selectedAudioIndex);
|
||||||
|
dialogBinding.audioStreamSpinner.setVisibility(View.VISIBLE);
|
||||||
|
dialogBinding.audioTrackSpinner.setVisibility(
|
||||||
|
wrappedAudioTracks.size() > 1 ? View.VISIBLE : View.GONE);
|
||||||
|
dialogBinding.audioTrackPresentInVideoText.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupVideoSpinner() {
|
private void setupVideoSpinner() {
|
||||||
@ -422,7 +458,19 @@ public class DownloadDialog extends DialogFragment
|
|||||||
|
|
||||||
dialogBinding.qualitySpinner.setAdapter(videoStreamsAdapter);
|
dialogBinding.qualitySpinner.setAdapter(videoStreamsAdapter);
|
||||||
dialogBinding.qualitySpinner.setSelection(selectedVideoIndex);
|
dialogBinding.qualitySpinner.setSelection(selectedVideoIndex);
|
||||||
|
dialogBinding.qualitySpinner.setVisibility(View.VISIBLE);
|
||||||
setRadioButtonsState(true);
|
setRadioButtonsState(true);
|
||||||
|
dialogBinding.audioStreamSpinner.setVisibility(View.GONE);
|
||||||
|
onVideoStreamSelected();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onVideoStreamSelected() {
|
||||||
|
final boolean isVideoOnly = videoStreamsAdapter.getItem(selectedVideoIndex).isVideoOnly();
|
||||||
|
|
||||||
|
dialogBinding.audioTrackSpinner.setVisibility(
|
||||||
|
isVideoOnly && wrappedAudioTracks.size() > 1 ? View.VISIBLE : View.GONE);
|
||||||
|
dialogBinding.audioTrackPresentInVideoText.setVisibility(
|
||||||
|
!isVideoOnly && wrappedAudioTracks.size() > 1 ? View.VISIBLE : View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupSubtitleSpinner() {
|
private void setupSubtitleSpinner() {
|
||||||
@ -432,7 +480,11 @@ public class DownloadDialog extends DialogFragment
|
|||||||
|
|
||||||
dialogBinding.qualitySpinner.setAdapter(subtitleStreamsAdapter);
|
dialogBinding.qualitySpinner.setAdapter(subtitleStreamsAdapter);
|
||||||
dialogBinding.qualitySpinner.setSelection(selectedSubtitleIndex);
|
dialogBinding.qualitySpinner.setSelection(selectedSubtitleIndex);
|
||||||
|
dialogBinding.qualitySpinner.setVisibility(View.VISIBLE);
|
||||||
setRadioButtonsState(true);
|
setRadioButtonsState(true);
|
||||||
|
dialogBinding.audioStreamSpinner.setVisibility(View.GONE);
|
||||||
|
dialogBinding.audioTrackSpinner.setVisibility(View.GONE);
|
||||||
|
dialogBinding.audioTrackPresentInVideoText.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -550,18 +602,31 @@ public class DownloadDialog extends DialogFragment
|
|||||||
+ "parent = [" + parent + "], view = [" + view + "], "
|
+ "parent = [" + parent + "], view = [" + view + "], "
|
||||||
+ "position = [" + position + "], id = [" + id + "]");
|
+ "position = [" + position + "], id = [" + id + "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch (parent.getId()) {
|
||||||
|
case R.id.quality_spinner:
|
||||||
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
|
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
|
||||||
case R.id.audio_button:
|
|
||||||
selectedAudioIndex = position;
|
|
||||||
break;
|
|
||||||
case R.id.video_button:
|
case R.id.video_button:
|
||||||
selectedVideoIndex = position;
|
selectedVideoIndex = position;
|
||||||
|
onVideoStreamSelected();
|
||||||
break;
|
break;
|
||||||
case R.id.subtitle_button:
|
case R.id.subtitle_button:
|
||||||
selectedSubtitleIndex = position;
|
selectedSubtitleIndex = position;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
onItemSelectedSetFileName();
|
onItemSelectedSetFileName();
|
||||||
|
break;
|
||||||
|
case R.id.audio_track_spinner:
|
||||||
|
final boolean trackChanged = selectedAudioTrackIndex != position;
|
||||||
|
selectedAudioTrackIndex = position;
|
||||||
|
if (trackChanged) {
|
||||||
|
updateSecondaryStreams();
|
||||||
|
fetchStreamsSize();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case R.id.audio_stream_spinner:
|
||||||
|
selectedAudioIndex = position;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onItemSelectedSetFileName() {
|
private void onItemSelectedSetFileName() {
|
||||||
@ -607,6 +672,7 @@ public class DownloadDialog extends DialogFragment
|
|||||||
|
|
||||||
protected void setupDownloadOptions() {
|
protected void setupDownloadOptions() {
|
||||||
setRadioButtonsState(false);
|
setRadioButtonsState(false);
|
||||||
|
setupAudioTrackSpinner();
|
||||||
|
|
||||||
final boolean isVideoStreamsAvailable = videoStreamsAdapter.getCount() > 0;
|
final boolean isVideoStreamsAvailable = videoStreamsAdapter.getCount() > 0;
|
||||||
final boolean isAudioStreamsAvailable = audioStreamsAdapter.getCount() > 0;
|
final boolean isAudioStreamsAvailable = audioStreamsAdapter.getCount() > 0;
|
||||||
@ -657,6 +723,13 @@ public class DownloadDialog extends DialogFragment
|
|||||||
dialogBinding.subtitleButton.setEnabled(enabled);
|
dialogBinding.subtitleButton.setEnabled(enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private StreamSizeWrapper<AudioStream> getWrappedAudioStreams() {
|
||||||
|
if (selectedAudioTrackIndex < 0 || selectedAudioTrackIndex > wrappedAudioTracks.size()) {
|
||||||
|
return StreamSizeWrapper.empty();
|
||||||
|
}
|
||||||
|
return wrappedAudioTracks.getTracksList().get(selectedAudioTrackIndex);
|
||||||
|
}
|
||||||
|
|
||||||
private int getSubtitleIndexBy(@NonNull final List<SubtitlesStream> streams) {
|
private int getSubtitleIndexBy(@NonNull final List<SubtitlesStream> streams) {
|
||||||
final Localization preferredLocalization = NewPipe.getPreferredLocalization();
|
final Localization preferredLocalization = NewPipe.getPreferredLocalization();
|
||||||
|
|
||||||
@ -1013,7 +1086,6 @@ public class DownloadDialog extends DialogFragment
|
|||||||
psName = Postprocessing.ALGORITHM_WEBM_MUXER;
|
psName = Postprocessing.ALGORITHM_WEBM_MUXER;
|
||||||
}
|
}
|
||||||
|
|
||||||
psArgs = null;
|
|
||||||
final long videoSize = wrappedVideoStreams.getSizeInBytes(
|
final long videoSize = wrappedVideoStreams.getSizeInBytes(
|
||||||
(VideoStream) selectedStream);
|
(VideoStream) selectedStream);
|
||||||
|
|
||||||
|
@ -162,8 +162,12 @@ public final class VideoDetailFragment
|
|||||||
private boolean showRelatedItems;
|
private boolean showRelatedItems;
|
||||||
private boolean showDescription;
|
private boolean showDescription;
|
||||||
private String selectedTabTag;
|
private String selectedTabTag;
|
||||||
@AttrRes @NonNull final List<Integer> tabIcons = new ArrayList<>();
|
@AttrRes
|
||||||
@StringRes @NonNull final List<Integer> tabContentDescriptions = new ArrayList<>();
|
@NonNull
|
||||||
|
final List<Integer> tabIcons = new ArrayList<>();
|
||||||
|
@StringRes
|
||||||
|
@NonNull
|
||||||
|
final List<Integer> tabContentDescriptions = new ArrayList<>();
|
||||||
private boolean tabSettingsChanged = false;
|
private boolean tabSettingsChanged = false;
|
||||||
private int lastAppBarVerticalOffset = Integer.MAX_VALUE; // prevents useless updates
|
private int lastAppBarVerticalOffset = Integer.MAX_VALUE; // prevents useless updates
|
||||||
|
|
||||||
@ -1040,20 +1044,10 @@ public final class VideoDetailFragment
|
|||||||
player.setRecovery();
|
player.setRecovery();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!useExternalAudioPlayer) {
|
if (useExternalAudioPlayer) {
|
||||||
openNormalBackgroundPlayer(append);
|
showExternalAudioPlaybackDialog();
|
||||||
} else {
|
} else {
|
||||||
final List<AudioStream> audioStreams = getUrlAndNonTorrentStreams(
|
openNormalBackgroundPlayer(append);
|
||||||
currentInfo.getAudioStreams());
|
|
||||||
final int index = ListHelper.getDefaultAudioFormat(activity, audioStreams);
|
|
||||||
|
|
||||||
if (index == -1) {
|
|
||||||
Toast.makeText(activity, R.string.no_audio_streams_available_for_external_players,
|
|
||||||
Toast.LENGTH_SHORT).show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
startOnExternalPlayer(activity, currentInfo, audioStreams.get(index));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1106,7 +1100,7 @@ public final class VideoDetailFragment
|
|||||||
|
|
||||||
if (PreferenceManager.getDefaultSharedPreferences(activity)
|
if (PreferenceManager.getDefaultSharedPreferences(activity)
|
||||||
.getBoolean(this.getString(R.string.use_external_video_player_key), false)) {
|
.getBoolean(this.getString(R.string.use_external_video_player_key), false)) {
|
||||||
showExternalPlaybackDialog();
|
showExternalVideoPlaybackDialog();
|
||||||
} else {
|
} else {
|
||||||
replaceQueueIfUserConfirms(this::openMainPlayer);
|
replaceQueueIfUserConfirms(this::openMainPlayer);
|
||||||
}
|
}
|
||||||
@ -2112,7 +2106,7 @@ public final class VideoDetailFragment
|
|||||||
}).show();
|
}).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showExternalPlaybackDialog() {
|
private void showExternalVideoPlaybackDialog() {
|
||||||
if (currentInfo == null) {
|
if (currentInfo == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -2159,6 +2153,44 @@ public final class VideoDetailFragment
|
|||||||
builder.show();
|
builder.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void showExternalAudioPlaybackDialog() {
|
||||||
|
if (currentInfo == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<AudioStream> audioStreams = getUrlAndNonTorrentStreams(
|
||||||
|
currentInfo.getAudioStreams());
|
||||||
|
final List<AudioStream> audioTracks =
|
||||||
|
ListHelper.getFilteredAudioStreams(activity, audioStreams);
|
||||||
|
|
||||||
|
if (audioTracks.isEmpty()) {
|
||||||
|
Toast.makeText(activity, R.string.no_audio_streams_available_for_external_players,
|
||||||
|
Toast.LENGTH_SHORT).show();
|
||||||
|
} else if (audioTracks.size() == 1) {
|
||||||
|
startOnExternalPlayer(activity, currentInfo, audioTracks.get(0));
|
||||||
|
} else {
|
||||||
|
final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||||
|
builder.setTitle(R.string.select_audio_track_external_players);
|
||||||
|
builder.setNeutralButton(R.string.open_in_browser, (dialog, i) ->
|
||||||
|
ShareUtils.openUrlInBrowser(requireActivity(), url));
|
||||||
|
|
||||||
|
final int selectedAudioStream =
|
||||||
|
ListHelper.getDefaultAudioFormat(activity, audioTracks);
|
||||||
|
final CharSequence[] trackNames = audioTracks.stream()
|
||||||
|
.map(audioStream -> Localization.audioTrackName(activity, audioStream))
|
||||||
|
.toArray(CharSequence[]::new);
|
||||||
|
|
||||||
|
builder.setSingleChoiceItems(trackNames, selectedAudioStream, null);
|
||||||
|
builder.setNegativeButton(R.string.cancel, null);
|
||||||
|
builder.setPositiveButton(R.string.ok, (dialog, i) -> {
|
||||||
|
final int index = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
|
||||||
|
startOnExternalPlayer(activity, currentInfo,
|
||||||
|
audioTracks.get(index));
|
||||||
|
});
|
||||||
|
builder.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Remove unneeded information while waiting for a next task
|
* Remove unneeded information while waiting for a next task
|
||||||
* */
|
* */
|
||||||
|
@ -13,6 +13,7 @@ import android.provider.Settings;
|
|||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
|
import android.view.SubMenu;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.SeekBar;
|
import android.widget.SeekBar;
|
||||||
@ -27,11 +28,13 @@ import com.google.android.exoplayer2.PlaybackParameters;
|
|||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.databinding.ActivityPlayerQueueControlBinding;
|
import org.schabi.newpipe.databinding.ActivityPlayerQueueControlBinding;
|
||||||
|
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
|
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
|
||||||
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||||
import org.schabi.newpipe.player.event.PlayerEventListener;
|
import org.schabi.newpipe.player.event.PlayerEventListener;
|
||||||
import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
|
import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
|
||||||
|
import org.schabi.newpipe.player.mediaitem.MediaItemTag;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueueAdapter;
|
import org.schabi.newpipe.player.playqueue.PlayQueueAdapter;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||||
@ -44,6 +47,9 @@ import org.schabi.newpipe.util.PermissionHelper;
|
|||||||
import org.schabi.newpipe.util.ServiceHelper;
|
import org.schabi.newpipe.util.ServiceHelper;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
public final class PlayQueueActivity extends AppCompatActivity
|
public final class PlayQueueActivity extends AppCompatActivity
|
||||||
implements PlayerEventListener, SeekBar.OnSeekBarChangeListener,
|
implements PlayerEventListener, SeekBar.OnSeekBarChangeListener,
|
||||||
View.OnClickListener, PlaybackParameterDialog.Callback {
|
View.OnClickListener, PlaybackParameterDialog.Callback {
|
||||||
@ -52,6 +58,8 @@ public final class PlayQueueActivity extends AppCompatActivity
|
|||||||
|
|
||||||
private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80;
|
private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80;
|
||||||
|
|
||||||
|
private static final int MENU_ID_AUDIO_TRACK = 71;
|
||||||
|
|
||||||
private Player player;
|
private Player player;
|
||||||
|
|
||||||
private boolean serviceBound;
|
private boolean serviceBound;
|
||||||
@ -97,6 +105,7 @@ public final class PlayQueueActivity extends AppCompatActivity
|
|||||||
this.menu = m;
|
this.menu = m;
|
||||||
getMenuInflater().inflate(R.menu.menu_play_queue, m);
|
getMenuInflater().inflate(R.menu.menu_play_queue, m);
|
||||||
getMenuInflater().inflate(R.menu.menu_play_queue_bg, m);
|
getMenuInflater().inflate(R.menu.menu_play_queue_bg, m);
|
||||||
|
buildAudioTrackMenu();
|
||||||
onMaybeMuteChanged();
|
onMaybeMuteChanged();
|
||||||
// to avoid null reference
|
// to avoid null reference
|
||||||
if (player != null) {
|
if (player != null) {
|
||||||
@ -153,6 +162,12 @@ public final class PlayQueueActivity extends AppCompatActivity
|
|||||||
NavigationHelper.playOnBackgroundPlayer(this, player.getPlayQueue(), true);
|
NavigationHelper.playOnBackgroundPlayer(this, player.getPlayQueue(), true);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (item.getGroupId() == MENU_ID_AUDIO_TRACK) {
|
||||||
|
onAudioTrackClick(item.getItemId());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -591,4 +606,69 @@ public final class PlayQueueActivity extends AppCompatActivity
|
|||||||
item.setIcon(player.isMuted() ? R.drawable.ic_volume_off : R.drawable.ic_volume_up);
|
item.setIcon(player.isMuted() ? R.drawable.ic_volume_off : R.drawable.ic_volume_up);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAudioTrackUpdate() {
|
||||||
|
buildAudioTrackMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildAudioTrackMenu() {
|
||||||
|
if (menu == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final MenuItem audioTrackSelector = menu.findItem(R.id.action_audio_track);
|
||||||
|
final List<AudioStream> availableStreams =
|
||||||
|
Optional.ofNullable(player.getCurrentMetadata())
|
||||||
|
.flatMap(MediaItemTag::getMaybeAudioTrack)
|
||||||
|
.map(MediaItemTag.AudioTrack::getAudioStreams)
|
||||||
|
.orElse(null);
|
||||||
|
final Optional<AudioStream> selectedAudioStream = player.getSelectedAudioStream();
|
||||||
|
|
||||||
|
if (availableStreams == null || availableStreams.size() < 2
|
||||||
|
|| selectedAudioStream.isEmpty()) {
|
||||||
|
audioTrackSelector.setVisible(false);
|
||||||
|
} else {
|
||||||
|
final SubMenu audioTrackMenu = audioTrackSelector.getSubMenu();
|
||||||
|
audioTrackMenu.clear();
|
||||||
|
|
||||||
|
for (int i = 0; i < availableStreams.size(); i++) {
|
||||||
|
final AudioStream audioStream = availableStreams.get(i);
|
||||||
|
audioTrackMenu.add(MENU_ID_AUDIO_TRACK, i, Menu.NONE,
|
||||||
|
Localization.audioTrackName(this, audioStream));
|
||||||
|
}
|
||||||
|
|
||||||
|
final AudioStream s = selectedAudioStream.get();
|
||||||
|
final String trackName = Localization.audioTrackName(this, s);
|
||||||
|
audioTrackSelector.setTitle(
|
||||||
|
getString(R.string.play_queue_audio_track, trackName));
|
||||||
|
|
||||||
|
final String shortName = s.getAudioLocale() != null
|
||||||
|
? s.getAudioLocale().getLanguage() : trackName;
|
||||||
|
audioTrackSelector.setTitleCondensed(
|
||||||
|
shortName.substring(0, Math.min(shortName.length(), 2)));
|
||||||
|
audioTrackSelector.setVisible(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when an item from the audio track selector is selected.
|
||||||
|
*
|
||||||
|
* @param itemId index of the selected item
|
||||||
|
*/
|
||||||
|
private void onAudioTrackClick(final int itemId) {
|
||||||
|
if (player.getCurrentMetadata() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
player.getCurrentMetadata().getMaybeAudioTrack().ifPresent(audioTrack -> {
|
||||||
|
final List<AudioStream> availableStreams = audioTrack.getAudioStreams();
|
||||||
|
final int selectedStreamIndex = audioTrack.getSelectedAudioStreamIndex();
|
||||||
|
if (selectedStreamIndex == itemId || availableStreams.size() <= itemId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String newAudioTrack = availableStreams.get(itemId).getAudioTrackId();
|
||||||
|
player.setAudioTrack(newAudioTrack);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -86,6 +86,7 @@ import org.schabi.newpipe.databinding.PlayerBinding;
|
|||||||
import org.schabi.newpipe.error.ErrorInfo;
|
import org.schabi.newpipe.error.ErrorInfo;
|
||||||
import org.schabi.newpipe.error.ErrorUtil;
|
import org.schabi.newpipe.error.ErrorUtil;
|
||||||
import org.schabi.newpipe.error.UserAction;
|
import org.schabi.newpipe.error.UserAction;
|
||||||
|
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||||
@ -179,13 +180,18 @@ public final class Player implements PlaybackListener, Listener {
|
|||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
// play queue might be null e.g. while player is starting
|
// play queue might be null e.g. while player is starting
|
||||||
@Nullable private PlayQueue playQueue;
|
@Nullable
|
||||||
|
private PlayQueue playQueue;
|
||||||
|
|
||||||
@Nullable private MediaSourceManager playQueueManager;
|
@Nullable
|
||||||
|
private MediaSourceManager playQueueManager;
|
||||||
|
|
||||||
@Nullable private PlayQueueItem currentItem;
|
@Nullable
|
||||||
@Nullable private MediaItemTag currentMetadata;
|
private PlayQueueItem currentItem;
|
||||||
@Nullable private Bitmap currentThumbnail;
|
@Nullable
|
||||||
|
private MediaItemTag currentMetadata;
|
||||||
|
@Nullable
|
||||||
|
private Bitmap currentThumbnail;
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Player
|
// Player
|
||||||
@ -194,12 +200,17 @@ public final class Player implements PlaybackListener, Listener {
|
|||||||
private ExoPlayer simpleExoPlayer;
|
private ExoPlayer simpleExoPlayer;
|
||||||
private AudioReactor audioReactor;
|
private AudioReactor audioReactor;
|
||||||
|
|
||||||
@NonNull private final DefaultTrackSelector trackSelector;
|
@NonNull
|
||||||
@NonNull private final LoadController loadController;
|
private final DefaultTrackSelector trackSelector;
|
||||||
@NonNull private final DefaultRenderersFactory renderFactory;
|
@NonNull
|
||||||
|
private final LoadController loadController;
|
||||||
|
@NonNull
|
||||||
|
private final DefaultRenderersFactory renderFactory;
|
||||||
|
|
||||||
@NonNull private final VideoPlaybackResolver videoResolver;
|
@NonNull
|
||||||
@NonNull private final AudioPlaybackResolver audioResolver;
|
private final VideoPlaybackResolver videoResolver;
|
||||||
|
@NonNull
|
||||||
|
private final AudioPlaybackResolver audioResolver;
|
||||||
|
|
||||||
private final PlayerService service; //TODO try to remove and replace everything with context
|
private final PlayerService service; //TODO try to remove and replace everything with context
|
||||||
|
|
||||||
@ -224,24 +235,32 @@ public final class Player implements PlaybackListener, Listener {
|
|||||||
|
|
||||||
private BroadcastReceiver broadcastReceiver;
|
private BroadcastReceiver broadcastReceiver;
|
||||||
private IntentFilter intentFilter;
|
private IntentFilter intentFilter;
|
||||||
@Nullable private PlayerServiceEventListener fragmentListener = null;
|
@Nullable
|
||||||
@Nullable private PlayerEventListener activityListener = null;
|
private PlayerServiceEventListener fragmentListener = null;
|
||||||
|
@Nullable
|
||||||
|
private PlayerEventListener activityListener = null;
|
||||||
|
|
||||||
@NonNull private final SerialDisposable progressUpdateDisposable = new SerialDisposable();
|
@NonNull
|
||||||
@NonNull private final CompositeDisposable databaseUpdateDisposable = new CompositeDisposable();
|
private final SerialDisposable progressUpdateDisposable = new SerialDisposable();
|
||||||
|
@NonNull
|
||||||
|
private final CompositeDisposable databaseUpdateDisposable = new CompositeDisposable();
|
||||||
|
|
||||||
// This is the only listener we need for thumbnail loading, since there is always at most only
|
// This is the only listener we need for thumbnail loading, since there is always at most only
|
||||||
// one thumbnail being loaded at a time. This field is also here to maintain a strong reference,
|
// one thumbnail being loaded at a time. This field is also here to maintain a strong reference,
|
||||||
// which would otherwise be garbage collected since Picasso holds weak references to targets.
|
// which would otherwise be garbage collected since Picasso holds weak references to targets.
|
||||||
@NonNull private final Target currentThumbnailTarget;
|
@NonNull
|
||||||
|
private final Target currentThumbnailTarget;
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Utils
|
// Utils
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@NonNull private final Context context;
|
@NonNull
|
||||||
@NonNull private final SharedPreferences prefs;
|
private final Context context;
|
||||||
@NonNull private final HistoryRecordManager recordManager;
|
@NonNull
|
||||||
|
private final SharedPreferences prefs;
|
||||||
|
@NonNull
|
||||||
|
private final HistoryRecordManager recordManager;
|
||||||
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
@ -333,7 +352,7 @@ public final class Player implements PlaybackListener, Listener {
|
|||||||
isAudioOnly = audioPlayerSelected();
|
isAudioOnly = audioPlayerSelected();
|
||||||
|
|
||||||
if (intent.hasExtra(PLAYBACK_QUALITY)) {
|
if (intent.hasExtra(PLAYBACK_QUALITY)) {
|
||||||
setPlaybackQuality(intent.getStringExtra(PLAYBACK_QUALITY));
|
videoResolver.setPlaybackQuality(intent.getStringExtra(PLAYBACK_QUALITY));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve enqueue intents
|
// Resolve enqueue intents
|
||||||
@ -922,7 +941,6 @@ public final class Player implements PlaybackListener, Listener {
|
|||||||
//endregion
|
//endregion
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Playback states
|
// Playback states
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
@ -1244,6 +1262,9 @@ public final class Player implements PlaybackListener, Listener {
|
|||||||
}
|
}
|
||||||
final StreamInfo previousInfo = Optional.ofNullable(currentMetadata)
|
final StreamInfo previousInfo = Optional.ofNullable(currentMetadata)
|
||||||
.flatMap(MediaItemTag::getMaybeStreamInfo).orElse(null);
|
.flatMap(MediaItemTag::getMaybeStreamInfo).orElse(null);
|
||||||
|
final MediaItemTag.AudioTrack previousAudioTrack =
|
||||||
|
Optional.ofNullable(currentMetadata)
|
||||||
|
.flatMap(MediaItemTag::getMaybeAudioTrack).orElse(null);
|
||||||
currentMetadata = tag;
|
currentMetadata = tag;
|
||||||
|
|
||||||
if (!currentMetadata.getErrors().isEmpty()) {
|
if (!currentMetadata.getErrors().isEmpty()) {
|
||||||
@ -1264,6 +1285,12 @@ public final class Player implements PlaybackListener, Listener {
|
|||||||
if (previousInfo == null || !previousInfo.getUrl().equals(info.getUrl())) {
|
if (previousInfo == null || !previousInfo.getUrl().equals(info.getUrl())) {
|
||||||
// only update with the new stream info if it has actually changed
|
// only update with the new stream info if it has actually changed
|
||||||
updateMetadataWith(info);
|
updateMetadataWith(info);
|
||||||
|
} else if (previousAudioTrack == null
|
||||||
|
|| tag.getMaybeAudioTrack()
|
||||||
|
.map(t -> t.getSelectedAudioStreamIndex()
|
||||||
|
!= previousAudioTrack.getSelectedAudioStreamIndex())
|
||||||
|
.orElse(false)) {
|
||||||
|
notifyAudioTrackUpdateToListeners();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -1351,6 +1378,7 @@ public final class Player implements PlaybackListener, Listener {
|
|||||||
// Errors
|
// Errors
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
//region Errors
|
//region Errors
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process exceptions produced by {@link com.google.android.exoplayer2.ExoPlayer ExoPlayer}.
|
* Process exceptions produced by {@link com.google.android.exoplayer2.ExoPlayer ExoPlayer}.
|
||||||
* <p>There are multiple types of errors:</p>
|
* <p>There are multiple types of errors:</p>
|
||||||
@ -1377,8 +1405,9 @@ public final class Player implements PlaybackListener, Listener {
|
|||||||
* For any error above that is <b>not</b> explicitly <b>catchable</b>, the player will
|
* For any error above that is <b>not</b> explicitly <b>catchable</b>, the player will
|
||||||
* create a notification so users are aware.
|
* create a notification so users are aware.
|
||||||
* </ul>
|
* </ul>
|
||||||
|
*
|
||||||
* @see com.google.android.exoplayer2.Player.Listener#onPlayerError(PlaybackException)
|
* @see com.google.android.exoplayer2.Player.Listener#onPlayerError(PlaybackException)
|
||||||
* */
|
*/
|
||||||
// Any error code not explicitly covered here are either unrelated to NewPipe use case
|
// Any error code not explicitly covered here are either unrelated to NewPipe use case
|
||||||
// (e.g. DRM) or not recoverable (e.g. Decoder error). In both cases, the player should
|
// (e.g. DRM) or not recoverable (e.g. Decoder error). In both cases, the player should
|
||||||
// shutdown.
|
// shutdown.
|
||||||
@ -1760,6 +1789,7 @@ public final class Player implements PlaybackListener, Listener {
|
|||||||
registerStreamViewed();
|
registerStreamViewed();
|
||||||
|
|
||||||
notifyMetadataUpdateToListeners();
|
notifyMetadataUpdateToListeners();
|
||||||
|
notifyAudioTrackUpdateToListeners();
|
||||||
UIs.call(playerUi -> playerUi.onMetadataChanged(info));
|
UIs.call(playerUi -> playerUi.onMetadataChanged(info));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1888,6 +1918,12 @@ public final class Player implements PlaybackListener, Listener {
|
|||||||
.map(quality -> quality.getSortedVideoStreams()
|
.map(quality -> quality.getSortedVideoStreams()
|
||||||
.get(quality.getSelectedVideoStreamIndex()));
|
.get(quality.getSelectedVideoStreamIndex()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Optional<AudioStream> getSelectedAudioStream() {
|
||||||
|
return Optional.ofNullable(currentMetadata)
|
||||||
|
.flatMap(MediaItemTag::getMaybeAudioTrack)
|
||||||
|
.map(MediaItemTag.AudioTrack::getSelectedAudioStream);
|
||||||
|
}
|
||||||
//endregion
|
//endregion
|
||||||
|
|
||||||
|
|
||||||
@ -2019,6 +2055,15 @@ public final class Player implements PlaybackListener, Listener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void notifyAudioTrackUpdateToListeners() {
|
||||||
|
if (fragmentListener != null) {
|
||||||
|
fragmentListener.onAudioTrackUpdate();
|
||||||
|
}
|
||||||
|
if (activityListener != null) {
|
||||||
|
activityListener.onAudioTrackUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void useVideoSource(final boolean videoEnabled) {
|
public void useVideoSource(final boolean videoEnabled) {
|
||||||
if (playQueue == null || isAudioOnly == !videoEnabled || audioPlayerSelected()) {
|
if (playQueue == null || isAudioOnly == !videoEnabled || audioPlayerSelected()) {
|
||||||
return;
|
return;
|
||||||
@ -2177,7 +2222,18 @@ public final class Player implements PlaybackListener, Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void setPlaybackQuality(@Nullable final String quality) {
|
public void setPlaybackQuality(@Nullable final String quality) {
|
||||||
|
saveStreamProgressState();
|
||||||
|
setRecovery();
|
||||||
videoResolver.setPlaybackQuality(quality);
|
videoResolver.setPlaybackQuality(quality);
|
||||||
|
reloadPlayQueueManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAudioTrack(@Nullable final String audioTrackId) {
|
||||||
|
saveStreamProgressState();
|
||||||
|
setRecovery();
|
||||||
|
videoResolver.setAudioTrack(audioTrackId);
|
||||||
|
audioResolver.setAudioTrack(audioTrackId);
|
||||||
|
reloadPlayQueueManager();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -2255,7 +2311,7 @@ public final class Player implements PlaybackListener, Listener {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the video renderer index of the current playing stream.
|
* Get the video renderer index of the current playing stream.
|
||||||
*
|
* <p>
|
||||||
* This method returns the video renderer index of the current
|
* This method returns the video renderer index of the current
|
||||||
* {@link MappingTrackSelector.MappedTrackInfo} or {@link #RENDERER_UNAVAILABLE} if the current
|
* {@link MappingTrackSelector.MappedTrackInfo} or {@link #RENDERER_UNAVAILABLE} if the current
|
||||||
* {@link MappingTrackSelector.MappedTrackInfo} is null or if there is no video renderer index.
|
* {@link MappingTrackSelector.MappedTrackInfo} is null or if there is no video renderer index.
|
||||||
|
@ -11,5 +11,6 @@ public interface PlayerEventListener {
|
|||||||
PlaybackParameters parameters);
|
PlaybackParameters parameters);
|
||||||
void onProgressUpdate(int currentProgress, int duration, int bufferPercent);
|
void onProgressUpdate(int currentProgress, int duration, int bufferPercent);
|
||||||
void onMetadataUpdate(StreamInfo info, PlayQueue queue);
|
void onMetadataUpdate(StreamInfo info, PlayQueue queue);
|
||||||
|
default void onAudioTrackUpdate() { }
|
||||||
void onServiceStopped();
|
void onServiceStopped();
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import com.google.android.exoplayer2.MediaItem.RequestMetadata;
|
|||||||
import com.google.android.exoplayer2.MediaMetadata;
|
import com.google.android.exoplayer2.MediaMetadata;
|
||||||
import com.google.android.exoplayer2.Player;
|
import com.google.android.exoplayer2.Player;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||||
@ -55,6 +56,11 @@ public interface MediaItemTag {
|
|||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
default Optional<AudioTrack> getMaybeAudioTrack() {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
<T> Optional<T> getMaybeExtras(@NonNull Class<T> type);
|
<T> Optional<T> getMaybeExtras(@NonNull Class<T> type);
|
||||||
|
|
||||||
<T> MediaItemTag withExtras(@NonNull T extra);
|
<T> MediaItemTag withExtras(@NonNull T extra);
|
||||||
@ -128,4 +134,37 @@ public interface MediaItemTag {
|
|||||||
? null : sortedVideoStreams.get(selectedVideoStreamIndex);
|
? null : sortedVideoStreams.get(selectedVideoStreamIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final class AudioTrack {
|
||||||
|
@NonNull
|
||||||
|
private final List<AudioStream> audioStreams;
|
||||||
|
private final int selectedAudioStreamIndex;
|
||||||
|
|
||||||
|
private AudioTrack(@NonNull final List<AudioStream> audioStreams,
|
||||||
|
final int selectedAudioStreamIndex) {
|
||||||
|
this.audioStreams = audioStreams;
|
||||||
|
this.selectedAudioStreamIndex = selectedAudioStreamIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
static AudioTrack of(@NonNull final List<AudioStream> audioStreams,
|
||||||
|
final int selectedAudioStreamIndex) {
|
||||||
|
return new AudioTrack(audioStreams, selectedAudioStreamIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public List<AudioStream> getAudioStreams() {
|
||||||
|
return audioStreams;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSelectedAudioStreamIndex() {
|
||||||
|
return selectedAudioStreamIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public AudioStream getSelectedAudioStream() {
|
||||||
|
return selectedAudioStreamIndex < 0
|
||||||
|
|| selectedAudioStreamIndex >= audioStreams.size()
|
||||||
|
? null : audioStreams.get(selectedAudioStreamIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ package org.schabi.newpipe.player.mediaitem;
|
|||||||
|
|
||||||
import com.google.android.exoplayer2.MediaItem;
|
import com.google.android.exoplayer2.MediaItem;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||||
@ -25,25 +26,41 @@ public final class StreamInfoTag implements MediaItemTag {
|
|||||||
@Nullable
|
@Nullable
|
||||||
private final MediaItemTag.Quality quality;
|
private final MediaItemTag.Quality quality;
|
||||||
@Nullable
|
@Nullable
|
||||||
|
private final MediaItemTag.AudioTrack audioTrack;
|
||||||
|
@Nullable
|
||||||
private final Object extras;
|
private final Object extras;
|
||||||
|
|
||||||
private StreamInfoTag(@NonNull final StreamInfo streamInfo,
|
private StreamInfoTag(@NonNull final StreamInfo streamInfo,
|
||||||
@Nullable final MediaItemTag.Quality quality,
|
@Nullable final MediaItemTag.Quality quality,
|
||||||
|
@Nullable final MediaItemTag.AudioTrack audioTrack,
|
||||||
@Nullable final Object extras) {
|
@Nullable final Object extras) {
|
||||||
this.streamInfo = streamInfo;
|
this.streamInfo = streamInfo;
|
||||||
this.quality = quality;
|
this.quality = quality;
|
||||||
|
this.audioTrack = audioTrack;
|
||||||
this.extras = extras;
|
this.extras = extras;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static StreamInfoTag of(@NonNull final StreamInfo streamInfo,
|
public static StreamInfoTag of(@NonNull final StreamInfo streamInfo,
|
||||||
@NonNull final List<VideoStream> sortedVideoStreams,
|
@NonNull final List<VideoStream> sortedVideoStreams,
|
||||||
final int selectedVideoStreamIndex) {
|
final int selectedVideoStreamIndex,
|
||||||
|
@NonNull final List<AudioStream> audioStreams,
|
||||||
|
final int selectedAudioStreamIndex) {
|
||||||
final Quality quality = Quality.of(sortedVideoStreams, selectedVideoStreamIndex);
|
final Quality quality = Quality.of(sortedVideoStreams, selectedVideoStreamIndex);
|
||||||
return new StreamInfoTag(streamInfo, quality, null);
|
final AudioTrack audioTrack =
|
||||||
|
AudioTrack.of(audioStreams, selectedAudioStreamIndex);
|
||||||
|
return new StreamInfoTag(streamInfo, quality, audioTrack, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StreamInfoTag of(@NonNull final StreamInfo streamInfo,
|
||||||
|
@NonNull final List<AudioStream> audioStreams,
|
||||||
|
final int selectedAudioStreamIndex) {
|
||||||
|
final AudioTrack audioTrack =
|
||||||
|
AudioTrack.of(audioStreams, selectedAudioStreamIndex);
|
||||||
|
return new StreamInfoTag(streamInfo, null, audioTrack, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static StreamInfoTag of(@NonNull final StreamInfo streamInfo) {
|
public static StreamInfoTag of(@NonNull final StreamInfo streamInfo) {
|
||||||
return new StreamInfoTag(streamInfo, null, null);
|
return new StreamInfoTag(streamInfo, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -103,6 +120,12 @@ public final class StreamInfoTag implements MediaItemTag {
|
|||||||
return Optional.ofNullable(quality);
|
return Optional.ofNullable(quality);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Optional<AudioTrack> getMaybeAudioTrack() {
|
||||||
|
return Optional.ofNullable(audioTrack);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <T> Optional<T> getMaybeExtras(@NonNull final Class<T> type) {
|
public <T> Optional<T> getMaybeExtras(@NonNull final Class<T> type) {
|
||||||
return Optional.ofNullable(extras).map(type::cast);
|
return Optional.ofNullable(extras).map(type::cast);
|
||||||
@ -110,6 +133,6 @@ public final class StreamInfoTag implements MediaItemTag {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public StreamInfoTag withExtras(@NonNull final Object extra) {
|
public StreamInfoTag withExtras(@NonNull final Object extra) {
|
||||||
return new StreamInfoTag(streamInfo, quality, extra);
|
return new StreamInfoTag(streamInfo, quality, audioTrack, extra);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package org.schabi.newpipe.player.resolver;
|
package org.schabi.newpipe.player.resolver;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.util.ListHelper.getFilteredAudioStreams;
|
||||||
import static org.schabi.newpipe.util.ListHelper.getPlayableStreams;
|
import static org.schabi.newpipe.util.ListHelper.getPlayableStreams;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
@ -28,6 +29,8 @@ public class AudioPlaybackResolver implements PlaybackResolver {
|
|||||||
private final Context context;
|
private final Context context;
|
||||||
@NonNull
|
@NonNull
|
||||||
private final PlayerDataSource dataSource;
|
private final PlayerDataSource dataSource;
|
||||||
|
@Nullable
|
||||||
|
private String audioTrack;
|
||||||
|
|
||||||
public AudioPlaybackResolver(@NonNull final Context context,
|
public AudioPlaybackResolver(@NonNull final Context context,
|
||||||
@NonNull final PlayerDataSource dataSource) {
|
@NonNull final PlayerDataSource dataSource) {
|
||||||
@ -35,6 +38,13 @@ public class AudioPlaybackResolver implements PlaybackResolver {
|
|||||||
this.dataSource = dataSource;
|
this.dataSource = dataSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a media source providing audio. If a service has no separate {@link AudioStream}s we
|
||||||
|
* use a video stream as audio source to support audio background playback.
|
||||||
|
*
|
||||||
|
* @param info of the stream
|
||||||
|
* @return the audio source to use or null if none could be found
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
@Nullable
|
@Nullable
|
||||||
public MediaSource resolve(@NonNull final StreamInfo info) {
|
public MediaSource resolve(@NonNull final StreamInfo info) {
|
||||||
@ -43,12 +53,27 @@ public class AudioPlaybackResolver implements PlaybackResolver {
|
|||||||
return liveSource;
|
return liveSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
final Stream stream = getAudioSource(info);
|
final List<AudioStream> audioStreams =
|
||||||
if (stream == null) {
|
getFilteredAudioStreams(context, info.getAudioStreams());
|
||||||
|
final Stream stream;
|
||||||
|
final MediaItemTag tag;
|
||||||
|
|
||||||
|
if (!audioStreams.isEmpty()) {
|
||||||
|
final int audioIndex =
|
||||||
|
ListHelper.getAudioFormatIndex(context, audioStreams, audioTrack);
|
||||||
|
stream = getStreamForIndex(audioIndex, audioStreams);
|
||||||
|
tag = StreamInfoTag.of(info, audioStreams, audioIndex);
|
||||||
|
} else {
|
||||||
|
final List<VideoStream> videoStreams =
|
||||||
|
getPlayableStreams(info.getVideoStreams(), info.getServiceId());
|
||||||
|
if (!videoStreams.isEmpty()) {
|
||||||
|
final int index = ListHelper.getDefaultResolutionIndex(context, videoStreams);
|
||||||
|
stream = getStreamForIndex(index, videoStreams);
|
||||||
|
tag = StreamInfoTag.of(info);
|
||||||
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
final MediaItemTag tag = StreamInfoTag.of(info);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return PlaybackResolver.buildMediaSource(
|
return PlaybackResolver.buildMediaSource(
|
||||||
@ -59,31 +84,6 @@ public class AudioPlaybackResolver implements PlaybackResolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a stream to be played as audio. If a service has no separate {@link AudioStream}s we
|
|
||||||
* use a video stream as audio source to support audio background playback.
|
|
||||||
*
|
|
||||||
* @param info of the stream
|
|
||||||
* @return the audio source to use or null if none could be found
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
private Stream getAudioSource(@NonNull final StreamInfo info) {
|
|
||||||
final List<AudioStream> audioStreams = getPlayableStreams(
|
|
||||||
info.getAudioStreams(), info.getServiceId());
|
|
||||||
if (!audioStreams.isEmpty()) {
|
|
||||||
final int index = ListHelper.getDefaultAudioFormat(context, audioStreams);
|
|
||||||
return getStreamForIndex(index, audioStreams);
|
|
||||||
} else {
|
|
||||||
final List<VideoStream> videoStreams = getPlayableStreams(
|
|
||||||
info.getVideoStreams(), info.getServiceId());
|
|
||||||
if (!videoStreams.isEmpty()) {
|
|
||||||
final int index = ListHelper.getDefaultResolutionIndex(context, videoStreams);
|
|
||||||
return getStreamForIndex(index, videoStreams);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
Stream getStreamForIndex(final int index, @NonNull final List<? extends Stream> streams) {
|
Stream getStreamForIndex(final int index, @NonNull final List<? extends Stream> streams) {
|
||||||
if (index >= 0 && index < streams.size()) {
|
if (index >= 0 && index < streams.size()) {
|
||||||
@ -91,4 +91,13 @@ public class AudioPlaybackResolver implements PlaybackResolver {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public String getAudioTrack() {
|
||||||
|
return audioTrack;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAudioTrack(@Nullable final String audioLanguage) {
|
||||||
|
this.audioTrack = audioLanguage;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -156,6 +156,16 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
|
|||||||
cacheKey.append(audioStream.getAverageBitrate());
|
cacheKey.append(audioStream.getAverageBitrate());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (audioStream.getAudioTrackId() != null) {
|
||||||
|
cacheKey.append(" ");
|
||||||
|
cacheKey.append(audioStream.getAudioTrackId());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audioStream.getAudioLocale() != null) {
|
||||||
|
cacheKey.append(" ");
|
||||||
|
cacheKey.append(audioStream.getAudioLocale().getISO3Language());
|
||||||
|
}
|
||||||
|
|
||||||
return cacheKey.toString();
|
return cacheKey.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,6 +28,7 @@ import java.util.List;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
import static com.google.android.exoplayer2.C.TIME_UNSET;
|
import static com.google.android.exoplayer2.C.TIME_UNSET;
|
||||||
|
import static org.schabi.newpipe.util.ListHelper.getFilteredAudioStreams;
|
||||||
import static org.schabi.newpipe.util.ListHelper.getUrlAndNonTorrentStreams;
|
import static org.schabi.newpipe.util.ListHelper.getUrlAndNonTorrentStreams;
|
||||||
import static org.schabi.newpipe.util.ListHelper.getPlayableStreams;
|
import static org.schabi.newpipe.util.ListHelper.getPlayableStreams;
|
||||||
|
|
||||||
@ -44,6 +45,8 @@ public class VideoPlaybackResolver implements PlaybackResolver {
|
|||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private String playbackQuality;
|
private String playbackQuality;
|
||||||
|
@Nullable
|
||||||
|
private String audioTrack;
|
||||||
|
|
||||||
public enum SourceType {
|
public enum SourceType {
|
||||||
LIVE_STREAM,
|
LIVE_STREAM,
|
||||||
@ -74,19 +77,29 @@ public class VideoPlaybackResolver implements PlaybackResolver {
|
|||||||
final List<VideoStream> videoStreamsList = ListHelper.getSortedStreamVideosList(context,
|
final List<VideoStream> videoStreamsList = ListHelper.getSortedStreamVideosList(context,
|
||||||
getPlayableStreams(info.getVideoStreams(), info.getServiceId()),
|
getPlayableStreams(info.getVideoStreams(), info.getServiceId()),
|
||||||
getPlayableStreams(info.getVideoOnlyStreams(), info.getServiceId()), false, true);
|
getPlayableStreams(info.getVideoOnlyStreams(), info.getServiceId()), false, true);
|
||||||
final int index;
|
final List<AudioStream> audioStreamsList =
|
||||||
|
getFilteredAudioStreams(context, info.getAudioStreams());
|
||||||
|
|
||||||
|
final int videoIndex;
|
||||||
if (videoStreamsList.isEmpty()) {
|
if (videoStreamsList.isEmpty()) {
|
||||||
index = -1;
|
videoIndex = -1;
|
||||||
} else if (playbackQuality == null) {
|
} else if (playbackQuality == null) {
|
||||||
index = qualityResolver.getDefaultResolutionIndex(videoStreamsList);
|
videoIndex = qualityResolver.getDefaultResolutionIndex(videoStreamsList);
|
||||||
} else {
|
} else {
|
||||||
index = qualityResolver.getOverrideResolutionIndex(videoStreamsList,
|
videoIndex = qualityResolver.getOverrideResolutionIndex(videoStreamsList,
|
||||||
getPlaybackQuality());
|
getPlaybackQuality());
|
||||||
}
|
}
|
||||||
final MediaItemTag tag = StreamInfoTag.of(info, videoStreamsList, index);
|
|
||||||
|
final int audioIndex =
|
||||||
|
ListHelper.getAudioFormatIndex(context, audioStreamsList, audioTrack);
|
||||||
|
final MediaItemTag tag =
|
||||||
|
StreamInfoTag.of(info, videoStreamsList, videoIndex, audioStreamsList, audioIndex);
|
||||||
@Nullable final VideoStream video = tag.getMaybeQuality()
|
@Nullable final VideoStream video = tag.getMaybeQuality()
|
||||||
.map(MediaItemTag.Quality::getSelectedVideoStream)
|
.map(MediaItemTag.Quality::getSelectedVideoStream)
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
|
@Nullable final AudioStream audio = tag.getMaybeAudioTrack()
|
||||||
|
.map(MediaItemTag.AudioTrack::getSelectedAudioStream)
|
||||||
|
.orElse(null);
|
||||||
|
|
||||||
if (video != null) {
|
if (video != null) {
|
||||||
try {
|
try {
|
||||||
@ -99,15 +112,9 @@ public class VideoPlaybackResolver implements PlaybackResolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create optional audio stream source
|
|
||||||
final List<AudioStream> audioStreams = getPlayableStreams(
|
|
||||||
info.getAudioStreams(), info.getServiceId());
|
|
||||||
final AudioStream audio = audioStreams.isEmpty() ? null : audioStreams.get(
|
|
||||||
ListHelper.getDefaultAudioFormat(context, audioStreams));
|
|
||||||
|
|
||||||
// Use the audio stream if there is no video stream, or
|
// Use the audio stream if there is no video stream, or
|
||||||
// merge with audio stream in case if video does not contain audio
|
// merge with audio stream in case if video does not contain audio
|
||||||
if (audio != null && (video == null || video.isVideoOnly())) {
|
if (audio != null && (video == null || video.isVideoOnly() || audioTrack != null)) {
|
||||||
try {
|
try {
|
||||||
final MediaSource audioSource = PlaybackResolver.buildMediaSource(
|
final MediaSource audioSource = PlaybackResolver.buildMediaSource(
|
||||||
dataSource, audio, info, PlaybackResolver.cacheKeyOf(info, audio), tag);
|
dataSource, audio, info, PlaybackResolver.cacheKeyOf(info, audio), tag);
|
||||||
@ -180,6 +187,15 @@ public class VideoPlaybackResolver implements PlaybackResolver {
|
|||||||
this.playbackQuality = playbackQuality;
|
this.playbackQuality = playbackQuality;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public String getAudioTrack() {
|
||||||
|
return audioTrack;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAudioTrack(@Nullable final String audioLanguage) {
|
||||||
|
this.audioTrack = audioLanguage;
|
||||||
|
}
|
||||||
|
|
||||||
public interface QualityResolver {
|
public interface QualityResolver {
|
||||||
int getDefaultResolutionIndex(List<VideoStream> sortedVideos);
|
int getDefaultResolutionIndex(List<VideoStream> sortedVideos);
|
||||||
|
|
||||||
|
@ -63,6 +63,7 @@ import org.schabi.newpipe.App;
|
|||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.databinding.PlayerBinding;
|
import org.schabi.newpipe.databinding.PlayerBinding;
|
||||||
import org.schabi.newpipe.extractor.MediaFormat;
|
import org.schabi.newpipe.extractor.MediaFormat;
|
||||||
|
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||||
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
||||||
@ -78,6 +79,7 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
|||||||
import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper;
|
import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper;
|
||||||
import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHolder;
|
import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHolder;
|
||||||
import org.schabi.newpipe.util.DeviceUtils;
|
import org.schabi.newpipe.util.DeviceUtils;
|
||||||
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
@ -108,7 +110,8 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
|||||||
|
|
||||||
protected PlayerBinding binding;
|
protected PlayerBinding binding;
|
||||||
private final Handler controlsVisibilityHandler = new Handler(Looper.getMainLooper());
|
private final Handler controlsVisibilityHandler = new Handler(Looper.getMainLooper());
|
||||||
@Nullable private SurfaceHolderCallback surfaceHolderCallback;
|
@Nullable
|
||||||
|
private SurfaceHolderCallback surfaceHolderCallback;
|
||||||
boolean surfaceIsSetup = false;
|
boolean surfaceIsSetup = false;
|
||||||
|
|
||||||
|
|
||||||
@ -117,11 +120,13 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
|||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
private static final int POPUP_MENU_ID_QUALITY = 69;
|
private static final int POPUP_MENU_ID_QUALITY = 69;
|
||||||
|
private static final int POPUP_MENU_ID_AUDIO_TRACK = 70;
|
||||||
private static final int POPUP_MENU_ID_PLAYBACK_SPEED = 79;
|
private static final int POPUP_MENU_ID_PLAYBACK_SPEED = 79;
|
||||||
private static final int POPUP_MENU_ID_CAPTION = 89;
|
private static final int POPUP_MENU_ID_CAPTION = 89;
|
||||||
|
|
||||||
protected boolean isSomePopupMenuVisible = false;
|
protected boolean isSomePopupMenuVisible = false;
|
||||||
private PopupMenu qualityPopupMenu;
|
private PopupMenu qualityPopupMenu;
|
||||||
|
private PopupMenu audioTrackPopupMenu;
|
||||||
protected PopupMenu playbackSpeedPopupMenu;
|
protected PopupMenu playbackSpeedPopupMenu;
|
||||||
private PopupMenu captionPopupMenu;
|
private PopupMenu captionPopupMenu;
|
||||||
|
|
||||||
@ -173,6 +178,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
|||||||
R.style.DarkPopupMenu);
|
R.style.DarkPopupMenu);
|
||||||
|
|
||||||
qualityPopupMenu = new PopupMenu(themeWrapper, binding.qualityTextView);
|
qualityPopupMenu = new PopupMenu(themeWrapper, binding.qualityTextView);
|
||||||
|
audioTrackPopupMenu = new PopupMenu(themeWrapper, binding.audioTrackTextView);
|
||||||
playbackSpeedPopupMenu = new PopupMenu(context, binding.playbackSpeed);
|
playbackSpeedPopupMenu = new PopupMenu(context, binding.playbackSpeed);
|
||||||
captionPopupMenu = new PopupMenu(themeWrapper, binding.captionTextView);
|
captionPopupMenu = new PopupMenu(themeWrapper, binding.captionTextView);
|
||||||
|
|
||||||
@ -190,6 +196,8 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
|||||||
|
|
||||||
protected void initListeners() {
|
protected void initListeners() {
|
||||||
binding.qualityTextView.setOnClickListener(makeOnClickListener(this::onQualityClicked));
|
binding.qualityTextView.setOnClickListener(makeOnClickListener(this::onQualityClicked));
|
||||||
|
binding.audioTrackTextView.setOnClickListener(
|
||||||
|
makeOnClickListener(this::onAudioTracksClicked));
|
||||||
binding.playbackSpeed.setOnClickListener(makeOnClickListener(this::onPlaybackSpeedClicked));
|
binding.playbackSpeed.setOnClickListener(makeOnClickListener(this::onPlaybackSpeedClicked));
|
||||||
|
|
||||||
binding.playbackSeekBar.setOnSeekBarChangeListener(this);
|
binding.playbackSeekBar.setOnSeekBarChangeListener(this);
|
||||||
@ -266,6 +274,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
|||||||
|
|
||||||
protected void deinitListeners() {
|
protected void deinitListeners() {
|
||||||
binding.qualityTextView.setOnClickListener(null);
|
binding.qualityTextView.setOnClickListener(null);
|
||||||
|
binding.audioTrackTextView.setOnClickListener(null);
|
||||||
binding.playbackSpeed.setOnClickListener(null);
|
binding.playbackSpeed.setOnClickListener(null);
|
||||||
binding.playbackSeekBar.setOnSeekBarChangeListener(null);
|
binding.playbackSeekBar.setOnSeekBarChangeListener(null);
|
||||||
binding.captionTextView.setOnClickListener(null);
|
binding.captionTextView.setOnClickListener(null);
|
||||||
@ -419,6 +428,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
|||||||
binding.topControls.setPaddingRelative(controlsPad, playerTopPad, controlsPad, 0);
|
binding.topControls.setPaddingRelative(controlsPad, playerTopPad, controlsPad, 0);
|
||||||
binding.bottomControls.setPaddingRelative(controlsPad, 0, controlsPad, 0);
|
binding.bottomControls.setPaddingRelative(controlsPad, 0, controlsPad, 0);
|
||||||
binding.qualityTextView.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad);
|
binding.qualityTextView.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad);
|
||||||
|
binding.audioTrackTextView.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad);
|
||||||
binding.playbackSpeed.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad);
|
binding.playbackSpeed.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad);
|
||||||
binding.playbackSpeed.setMinimumWidth(buttonsMinWidth);
|
binding.playbackSpeed.setMinimumWidth(buttonsMinWidth);
|
||||||
binding.captionTextView.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad);
|
binding.captionTextView.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad);
|
||||||
@ -524,6 +534,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the current duration into the corresponding elements.
|
* Sets the current duration into the corresponding elements.
|
||||||
|
*
|
||||||
* @param currentProgress the current progress, in milliseconds
|
* @param currentProgress the current progress, in milliseconds
|
||||||
*/
|
*/
|
||||||
private void updatePlayBackElementsCurrentDuration(final int currentProgress) {
|
private void updatePlayBackElementsCurrentDuration(final int currentProgress) {
|
||||||
@ -536,6 +547,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the video duration time into all control components (e.g. seekbar).
|
* Sets the video duration time into all control components (e.g. seekbar).
|
||||||
|
*
|
||||||
* @param duration the video duration, in milliseconds
|
* @param duration the video duration, in milliseconds
|
||||||
*/
|
*/
|
||||||
private void setVideoDurationToControls(final int duration) {
|
private void setVideoDurationToControls(final int duration) {
|
||||||
@ -984,6 +996,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
|||||||
private void updateStreamRelatedViews() {
|
private void updateStreamRelatedViews() {
|
||||||
player.getCurrentStreamInfo().ifPresent(info -> {
|
player.getCurrentStreamInfo().ifPresent(info -> {
|
||||||
binding.qualityTextView.setVisibility(View.GONE);
|
binding.qualityTextView.setVisibility(View.GONE);
|
||||||
|
binding.audioTrackTextView.setVisibility(View.GONE);
|
||||||
binding.playbackSpeed.setVisibility(View.GONE);
|
binding.playbackSpeed.setVisibility(View.GONE);
|
||||||
|
|
||||||
binding.playbackEndTime.setVisibility(View.GONE);
|
binding.playbackEndTime.setVisibility(View.GONE);
|
||||||
@ -1019,6 +1032,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
|||||||
}
|
}
|
||||||
|
|
||||||
buildQualityMenu();
|
buildQualityMenu();
|
||||||
|
buildAudioTrackMenu();
|
||||||
|
|
||||||
binding.qualityTextView.setVisibility(View.VISIBLE);
|
binding.qualityTextView.setVisibility(View.VISIBLE);
|
||||||
binding.surfaceView.setVisibility(View.VISIBLE);
|
binding.surfaceView.setVisibility(View.VISIBLE);
|
||||||
@ -1067,6 +1081,34 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
|||||||
.ifPresent(s -> binding.qualityTextView.setText(s.getResolution()));
|
.ifPresent(s -> binding.qualityTextView.setText(s.getResolution()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void buildAudioTrackMenu() {
|
||||||
|
if (audioTrackPopupMenu == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
audioTrackPopupMenu.getMenu().removeGroup(POPUP_MENU_ID_AUDIO_TRACK);
|
||||||
|
|
||||||
|
final List<AudioStream> availableStreams = Optional.ofNullable(player.getCurrentMetadata())
|
||||||
|
.flatMap(MediaItemTag::getMaybeAudioTrack)
|
||||||
|
.map(MediaItemTag.AudioTrack::getAudioStreams)
|
||||||
|
.orElse(null);
|
||||||
|
if (availableStreams == null || availableStreams.size() < 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < availableStreams.size(); i++) {
|
||||||
|
final AudioStream audioStream = availableStreams.get(i);
|
||||||
|
audioTrackPopupMenu.getMenu().add(POPUP_MENU_ID_AUDIO_TRACK, i, Menu.NONE,
|
||||||
|
Localization.audioTrackName(context, audioStream));
|
||||||
|
}
|
||||||
|
|
||||||
|
player.getSelectedAudioStream()
|
||||||
|
.ifPresent(s -> binding.audioTrackTextView.setText(
|
||||||
|
Localization.audioTrackName(context, s)));
|
||||||
|
binding.audioTrackTextView.setVisibility(View.VISIBLE);
|
||||||
|
audioTrackPopupMenu.setOnMenuItemClickListener(this);
|
||||||
|
audioTrackPopupMenu.setOnDismissListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
private void buildPlaybackSpeedMenu() {
|
private void buildPlaybackSpeedMenu() {
|
||||||
if (playbackSpeedPopupMenu == null) {
|
if (playbackSpeedPopupMenu == null) {
|
||||||
return;
|
return;
|
||||||
@ -1175,6 +1217,11 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
|||||||
.ifPresent(binding.qualityTextView::setText);
|
.ifPresent(binding.qualityTextView::setText);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void onAudioTracksClicked() {
|
||||||
|
audioTrackPopupMenu.show();
|
||||||
|
isSomePopupMenuVisible = true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when an item of the quality selector or the playback speed selector is selected.
|
* Called when an item of the quality selector or the playback speed selector is selected.
|
||||||
*/
|
*/
|
||||||
@ -1187,26 +1234,10 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (menuItem.getGroupId() == POPUP_MENU_ID_QUALITY) {
|
if (menuItem.getGroupId() == POPUP_MENU_ID_QUALITY) {
|
||||||
final int menuItemIndex = menuItem.getItemId();
|
onQualityItemClick(menuItem);
|
||||||
@Nullable final MediaItemTag currentMetadata = player.getCurrentMetadata();
|
|
||||||
if (currentMetadata == null || currentMetadata.getMaybeQuality().isEmpty()) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
} else if (menuItem.getGroupId() == POPUP_MENU_ID_AUDIO_TRACK) {
|
||||||
|
onAudioTrackItemClick(menuItem);
|
||||||
final MediaItemTag.Quality quality = currentMetadata.getMaybeQuality().get();
|
|
||||||
final List<VideoStream> availableStreams = quality.getSortedVideoStreams();
|
|
||||||
final int selectedStreamIndex = quality.getSelectedVideoStreamIndex();
|
|
||||||
if (selectedStreamIndex == menuItemIndex || availableStreams.size() <= menuItemIndex) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
player.saveStreamProgressState(); //TODO added, check if good
|
|
||||||
final String newResolution = availableStreams.get(menuItemIndex).getResolution();
|
|
||||||
player.setRecovery();
|
|
||||||
player.setPlaybackQuality(newResolution);
|
|
||||||
player.reloadPlayQueueManager();
|
|
||||||
|
|
||||||
binding.qualityTextView.setText(menuItem.getTitle());
|
|
||||||
return true;
|
return true;
|
||||||
} else if (menuItem.getGroupId() == POPUP_MENU_ID_PLAYBACK_SPEED) {
|
} else if (menuItem.getGroupId() == POPUP_MENU_ID_PLAYBACK_SPEED) {
|
||||||
final int speedIndex = menuItem.getItemId();
|
final int speedIndex = menuItem.getItemId();
|
||||||
@ -1219,6 +1250,47 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void onQualityItemClick(@NonNull final MenuItem menuItem) {
|
||||||
|
final int menuItemIndex = menuItem.getItemId();
|
||||||
|
@Nullable final MediaItemTag currentMetadata = player.getCurrentMetadata();
|
||||||
|
if (currentMetadata == null || currentMetadata.getMaybeQuality().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final MediaItemTag.Quality quality = currentMetadata.getMaybeQuality().get();
|
||||||
|
final List<VideoStream> availableStreams = quality.getSortedVideoStreams();
|
||||||
|
final int selectedStreamIndex = quality.getSelectedVideoStreamIndex();
|
||||||
|
if (selectedStreamIndex == menuItemIndex || availableStreams.size() <= menuItemIndex) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String newResolution = availableStreams.get(menuItemIndex).getResolution();
|
||||||
|
player.setPlaybackQuality(newResolution);
|
||||||
|
|
||||||
|
binding.qualityTextView.setText(menuItem.getTitle());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onAudioTrackItemClick(@NonNull final MenuItem menuItem) {
|
||||||
|
final int menuItemIndex = menuItem.getItemId();
|
||||||
|
@Nullable final MediaItemTag currentMetadata = player.getCurrentMetadata();
|
||||||
|
if (currentMetadata == null || currentMetadata.getMaybeAudioTrack().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final MediaItemTag.AudioTrack audioTrack =
|
||||||
|
currentMetadata.getMaybeAudioTrack().get();
|
||||||
|
final List<AudioStream> availableStreams = audioTrack.getAudioStreams();
|
||||||
|
final int selectedStreamIndex = audioTrack.getSelectedAudioStreamIndex();
|
||||||
|
if (selectedStreamIndex == menuItemIndex || availableStreams.size() <= menuItemIndex) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String newAudioTrack = availableStreams.get(menuItemIndex).getAudioTrackId();
|
||||||
|
player.setAudioTrack(newAudioTrack);
|
||||||
|
|
||||||
|
binding.audioTrackTextView.setText(menuItem.getTitle());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when some popup menu is dismissed.
|
* Called when some popup menu is dismissed.
|
||||||
*/
|
*/
|
||||||
|
@ -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.R;
|
||||||
import org.schabi.newpipe.extractor.MediaFormat;
|
import org.schabi.newpipe.extractor.MediaFormat;
|
||||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||||
|
import org.schabi.newpipe.extractor.stream.AudioTrackType;
|
||||||
import org.schabi.newpipe.extractor.stream.DeliveryMethod;
|
import org.schabi.newpipe.extractor.stream.DeliveryMethod;
|
||||||
import org.schabi.newpipe.extractor.stream.Stream;
|
import org.schabi.newpipe.extractor.stream.Stream;
|
||||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||||
@ -25,6 +26,7 @@ import java.util.Collections;
|
|||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
@ -38,11 +40,17 @@ public final class ListHelper {
|
|||||||
// Audio format in order of quality. 0=lowest quality, n=highest quality
|
// Audio format in order of quality. 0=lowest quality, n=highest quality
|
||||||
private static final List<MediaFormat> AUDIO_FORMAT_QUALITY_RANKING =
|
private static final List<MediaFormat> AUDIO_FORMAT_QUALITY_RANKING =
|
||||||
List.of(MediaFormat.MP3, MediaFormat.WEBMA, MediaFormat.M4A);
|
List.of(MediaFormat.MP3, MediaFormat.WEBMA, MediaFormat.M4A);
|
||||||
// Audio format in order of efficiency. 0=most efficient, n=least efficient
|
// Audio format in order of efficiency. 0=least efficient, n=most efficient
|
||||||
private static final List<MediaFormat> AUDIO_FORMAT_EFFICIENCY_RANKING =
|
private static final List<MediaFormat> AUDIO_FORMAT_EFFICIENCY_RANKING =
|
||||||
List.of(MediaFormat.WEBMA, MediaFormat.M4A, MediaFormat.MP3);
|
List.of(MediaFormat.MP3, MediaFormat.M4A, MediaFormat.WEBMA);
|
||||||
// Use a Set for better performance
|
// Use a Set for better performance
|
||||||
private static final Set<String> HIGH_RESOLUTION_LIST = Set.of("1440p", "2160p");
|
private static final Set<String> HIGH_RESOLUTION_LIST = Set.of("1440p", "2160p");
|
||||||
|
// Audio track types in order of priotity. 0=lowest, n=highest
|
||||||
|
private static final List<AudioTrackType> AUDIO_TRACK_TYPE_RANKING =
|
||||||
|
List.of(AudioTrackType.DESCRIPTIVE, AudioTrackType.DUBBED, AudioTrackType.ORIGINAL);
|
||||||
|
// Audio track types in order of priotity when descriptive audio is preferred.
|
||||||
|
private static final List<AudioTrackType> AUDIO_TRACK_TYPE_RANKING_DESCRIPTIVE =
|
||||||
|
List.of(AudioTrackType.ORIGINAL, AudioTrackType.DUBBED, AudioTrackType.DESCRIPTIVE);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of supported YouTube Itag ids.
|
* List of supported YouTube Itag ids.
|
||||||
@ -62,10 +70,10 @@ public final class ListHelper {
|
|||||||
private ListHelper() { }
|
private ListHelper() { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see #getDefaultResolutionIndex(String, String, MediaFormat, List)
|
|
||||||
* @param context Android app context
|
* @param context Android app context
|
||||||
* @param videoStreams list of the video streams to check
|
* @param videoStreams list of the video streams to check
|
||||||
* @return index of the video stream with the default index
|
* @return index of the video stream with the default index
|
||||||
|
* @see #getDefaultResolutionIndex(String, String, MediaFormat, List)
|
||||||
*/
|
*/
|
||||||
public static int getDefaultResolutionIndex(final Context context,
|
public static int getDefaultResolutionIndex(final Context context,
|
||||||
final List<VideoStream> videoStreams) {
|
final List<VideoStream> videoStreams) {
|
||||||
@ -75,11 +83,11 @@ public final class ListHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see #getDefaultResolutionIndex(String, String, MediaFormat, List)
|
|
||||||
* @param context Android app context
|
* @param context Android app context
|
||||||
* @param videoStreams list of the video streams to check
|
* @param videoStreams list of the video streams to check
|
||||||
* @param defaultResolution the default resolution to look for
|
* @param defaultResolution the default resolution to look for
|
||||||
* @return index of the video stream with the default index
|
* @return index of the video stream with the default index
|
||||||
|
* @see #getDefaultResolutionIndex(String, String, MediaFormat, List)
|
||||||
*/
|
*/
|
||||||
public static int getResolutionIndex(final Context context,
|
public static int getResolutionIndex(final Context context,
|
||||||
final List<VideoStream> videoStreams,
|
final List<VideoStream> videoStreams,
|
||||||
@ -88,10 +96,10 @@ public final class ListHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see #getDefaultResolutionIndex(String, String, MediaFormat, List)
|
|
||||||
* @param context Android app context
|
* @param context Android app context
|
||||||
* @param videoStreams list of the video streams to check
|
* @param videoStreams list of the video streams to check
|
||||||
* @return index of the video stream with the default index
|
* @return index of the video stream with the default index
|
||||||
|
* @see #getDefaultResolutionIndex(String, String, MediaFormat, List)
|
||||||
*/
|
*/
|
||||||
public static int getPopupDefaultResolutionIndex(final Context context,
|
public static int getPopupDefaultResolutionIndex(final Context context,
|
||||||
final List<VideoStream> videoStreams) {
|
final List<VideoStream> videoStreams) {
|
||||||
@ -101,11 +109,11 @@ public final class ListHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see #getDefaultResolutionIndex(String, String, MediaFormat, List)
|
|
||||||
* @param context Android app context
|
* @param context Android app context
|
||||||
* @param videoStreams list of the video streams to check
|
* @param videoStreams list of the video streams to check
|
||||||
* @param defaultResolution the default resolution to look for
|
* @param defaultResolution the default resolution to look for
|
||||||
* @return index of the video stream with the default index
|
* @return index of the video stream with the default index
|
||||||
|
* @see #getDefaultResolutionIndex(String, String, MediaFormat, List)
|
||||||
*/
|
*/
|
||||||
public static int getPopupResolutionIndex(final Context context,
|
public static int getPopupResolutionIndex(final Context context,
|
||||||
final List<VideoStream> videoStreams,
|
final List<VideoStream> videoStreams,
|
||||||
@ -115,16 +123,36 @@ public final class ListHelper {
|
|||||||
|
|
||||||
public static int getDefaultAudioFormat(final Context context,
|
public static int getDefaultAudioFormat(final Context context,
|
||||||
final List<AudioStream> audioStreams) {
|
final List<AudioStream> audioStreams) {
|
||||||
final MediaFormat defaultFormat = getDefaultFormat(context,
|
return getAudioIndexByHighestRank(audioStreams,
|
||||||
R.string.default_audio_format_key, R.string.default_audio_format_value);
|
getAudioTrackComparator(context).thenComparing(getAudioFormatComparator(context)));
|
||||||
|
|
||||||
// If the user has chosen to limit resolution to conserve mobile data
|
|
||||||
// usage then we should also limit our audio usage.
|
|
||||||
if (isLimitingDataUsage(context)) {
|
|
||||||
return getMostCompactAudioIndex(defaultFormat, audioStreams);
|
|
||||||
} else {
|
|
||||||
return getHighestQualityAudioIndex(defaultFormat, audioStreams);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static int getDefaultAudioTrackGroup(final Context context,
|
||||||
|
final List<List<AudioStream>> groupedAudioStreams) {
|
||||||
|
if (groupedAudioStreams == null || groupedAudioStreams.isEmpty()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Comparator<AudioStream> cmp = getAudioTrackComparator(context);
|
||||||
|
final List<AudioStream> highestRanked = groupedAudioStreams.stream()
|
||||||
|
.max((o1, o2) -> cmp.compare(o1.get(0), o2.get(0)))
|
||||||
|
.orElse(null);
|
||||||
|
return groupedAudioStreams.indexOf(highestRanked);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getAudioFormatIndex(final Context context,
|
||||||
|
final List<AudioStream> audioStreams,
|
||||||
|
@Nullable final String trackId) {
|
||||||
|
if (trackId != null) {
|
||||||
|
for (int i = 0; i < audioStreams.size(); i++) {
|
||||||
|
final AudioStream s = audioStreams.get(i);
|
||||||
|
if (s.getAudioTrackId() != null
|
||||||
|
&& s.getAudioTrackId().equals(trackId)) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return getDefaultAudioFormat(context, audioStreams);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -211,6 +239,90 @@ public final class ListHelper {
|
|||||||
videoOnlyStreams, ascendingOrder, preferVideoOnlyStreams);
|
videoOnlyStreams, ascendingOrder, preferVideoOnlyStreams);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter the list of audio streams and return a list with the preferred stream for
|
||||||
|
* each audio track. Streams are sorted with the preferred language in the first position.
|
||||||
|
*
|
||||||
|
* @param context the context to search for the track to give preference
|
||||||
|
* @param audioStreams the list of audio streams
|
||||||
|
* @return the sorted, filtered list
|
||||||
|
*/
|
||||||
|
public static List<AudioStream> getFilteredAudioStreams(
|
||||||
|
@NonNull final Context context,
|
||||||
|
@Nullable final List<AudioStream> audioStreams) {
|
||||||
|
if (audioStreams == null) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
final HashMap<String, AudioStream> collectedStreams = new HashMap<>();
|
||||||
|
|
||||||
|
final Comparator<AudioStream> cmp = getAudioFormatComparator(context);
|
||||||
|
|
||||||
|
for (final AudioStream stream : audioStreams) {
|
||||||
|
if (stream.getDeliveryMethod() == DeliveryMethod.TORRENT) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String trackId = Objects.toString(stream.getAudioTrackId(), "");
|
||||||
|
|
||||||
|
final AudioStream presentStream = collectedStreams.get(trackId);
|
||||||
|
if (presentStream == null || cmp.compare(stream, presentStream) > 0) {
|
||||||
|
collectedStreams.put(trackId, stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter unknown audio tracks if there are multiple tracks
|
||||||
|
if (collectedStreams.size() > 1) {
|
||||||
|
collectedStreams.remove("");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort collected streams by name
|
||||||
|
return collectedStreams.values().stream().sorted(getAudioTrackNameComparator(context))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Group the list of audioStreams by their track ID and sort the resulting list by track name.
|
||||||
|
*
|
||||||
|
* @param context app context to get track names for sorting
|
||||||
|
* @param audioStreams list of audio streams
|
||||||
|
* @return list of audio streams lists representing individual tracks
|
||||||
|
*/
|
||||||
|
public static List<List<AudioStream>> getGroupedAudioStreams(
|
||||||
|
@NonNull final Context context,
|
||||||
|
@Nullable final List<AudioStream> audioStreams) {
|
||||||
|
if (audioStreams == null) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
final HashMap<String, List<AudioStream>> collectedStreams = new HashMap<>();
|
||||||
|
|
||||||
|
for (final AudioStream stream : audioStreams) {
|
||||||
|
final String trackId = Objects.toString(stream.getAudioTrackId(), "");
|
||||||
|
if (collectedStreams.containsKey(trackId)) {
|
||||||
|
collectedStreams.get(trackId).add(stream);
|
||||||
|
} else {
|
||||||
|
final List<AudioStream> list = new ArrayList<>();
|
||||||
|
list.add(stream);
|
||||||
|
collectedStreams.put(trackId, list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter unknown audio tracks if there are multiple tracks
|
||||||
|
if (collectedStreams.size() > 1) {
|
||||||
|
collectedStreams.remove("");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort tracks alphabetically, sort track streams by quality
|
||||||
|
final Comparator<AudioStream> nameCmp = getAudioTrackNameComparator(context);
|
||||||
|
final Comparator<AudioStream> formatCmp = getAudioFormatComparator(context);
|
||||||
|
|
||||||
|
return collectedStreams.values().stream()
|
||||||
|
.sorted((o1, o2) -> nameCmp.compare(o1.get(0), o2.get(0)))
|
||||||
|
.map(streams -> streams.stream().sorted(formatCmp).collect(Collectors.toList()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Utils
|
// Utils
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
@ -376,72 +488,22 @@ public final class ListHelper {
|
|||||||
return videoStreams;
|
return videoStreams;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the audio from the list with the highest quality.
|
|
||||||
* Format will be ignored if it yields no results.
|
|
||||||
*
|
|
||||||
* @param format The target format type or null if it doesn't matter
|
|
||||||
* @param audioStreams List of audio streams
|
|
||||||
* @return Index of audio stream that produces the most compact results or -1 if not found
|
|
||||||
*/
|
|
||||||
static int getHighestQualityAudioIndex(@Nullable final MediaFormat format,
|
|
||||||
@Nullable final List<AudioStream> audioStreams) {
|
|
||||||
return getAudioIndexByHighestRank(format, audioStreams,
|
|
||||||
// Compares descending (last = highest rank)
|
|
||||||
getAudioStreamComparator(AUDIO_FORMAT_QUALITY_RANKING));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the audio from the list with the lowest bitrate and most efficient format.
|
|
||||||
* Format will be ignored if it yields no results.
|
|
||||||
*
|
|
||||||
* @param format The target format type or null if it doesn't matter
|
|
||||||
* @param audioStreams List of audio streams
|
|
||||||
* @return Index of audio stream that produces the most compact results or -1 if not found
|
|
||||||
*/
|
|
||||||
static int getMostCompactAudioIndex(@Nullable final MediaFormat format,
|
|
||||||
@Nullable final List<AudioStream> audioStreams) {
|
|
||||||
return getAudioIndexByHighestRank(format, audioStreams,
|
|
||||||
// The "reversed()" is important -> Compares ascending (first = highest rank)
|
|
||||||
getAudioStreamComparator(AUDIO_FORMAT_EFFICIENCY_RANKING).reversed());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Comparator<AudioStream> getAudioStreamComparator(
|
|
||||||
final List<MediaFormat> formatRanking) {
|
|
||||||
return Comparator.nullsLast(Comparator.comparingInt(AudioStream::getAverageBitrate))
|
|
||||||
.thenComparingInt(stream -> formatRanking.indexOf(stream.getFormat()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the audio-stream from the list with the highest rank, depending on the comparator.
|
* Get the audio-stream from the list with the highest rank, depending on the comparator.
|
||||||
* Format will be ignored if it yields no results.
|
* Format will be ignored if it yields no results.
|
||||||
*
|
*
|
||||||
* @param targetedFormat The target format type or null if it doesn't matter
|
|
||||||
* @param audioStreams List of audio streams
|
* @param audioStreams List of audio streams
|
||||||
* @param comparator The comparator used for determining the max/best/highest ranked value
|
* @param comparator The comparator used for determining the max/best/highest ranked value
|
||||||
* @return Index of audio stream that produces the highest ranked result or -1 if not found
|
* @return Index of audio stream that produces the highest ranked result or -1 if not found
|
||||||
*/
|
*/
|
||||||
private static int getAudioIndexByHighestRank(@Nullable final MediaFormat targetedFormat,
|
static int getAudioIndexByHighestRank(@Nullable final List<AudioStream> audioStreams,
|
||||||
@Nullable final List<AudioStream> audioStreams,
|
|
||||||
final Comparator<AudioStream> comparator) {
|
final Comparator<AudioStream> comparator) {
|
||||||
if (audioStreams == null || audioStreams.isEmpty()) {
|
if (audioStreams == null || audioStreams.isEmpty()) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
final AudioStream highestRankedAudioStream = audioStreams.stream()
|
final AudioStream highestRankedAudioStream = audioStreams.stream()
|
||||||
.filter(audioStream -> targetedFormat == null
|
.max(comparator).orElse(null);
|
||||||
|| audioStream.getFormat() == targetedFormat)
|
|
||||||
.max(comparator)
|
|
||||||
.orElse(null);
|
|
||||||
|
|
||||||
if (highestRankedAudioStream == null) {
|
|
||||||
// Fallback: Ignore targetedFormat if not null
|
|
||||||
if (targetedFormat != null) {
|
|
||||||
return getAudioIndexByHighestRank(null, audioStreams, comparator);
|
|
||||||
}
|
|
||||||
// targetedFormat is already null -> return -1
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return audioStreams.indexOf(highestRankedAudioStream);
|
return audioStreams.indexOf(highestRankedAudioStream);
|
||||||
}
|
}
|
||||||
@ -629,4 +691,149 @@ public final class ListHelper {
|
|||||||
|
|
||||||
return manager.isActiveNetworkMetered();
|
return manager.isActiveNetworkMetered();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a {@link Comparator} to compare {@link AudioStream}s by their format and bitrate.
|
||||||
|
*
|
||||||
|
* <p>The prefered stream will be ordered last.</p>
|
||||||
|
*
|
||||||
|
* @param context app context
|
||||||
|
* @return Comparator
|
||||||
|
*/
|
||||||
|
private static Comparator<AudioStream> getAudioFormatComparator(
|
||||||
|
final @NonNull Context context) {
|
||||||
|
final MediaFormat defaultFormat = getDefaultFormat(context,
|
||||||
|
R.string.default_audio_format_key, R.string.default_audio_format_value);
|
||||||
|
return getAudioFormatComparator(defaultFormat, isLimitingDataUsage(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a {@link Comparator} to compare {@link AudioStream}s by their format and bitrate.
|
||||||
|
*
|
||||||
|
* <p>The prefered stream will be ordered last.</p>
|
||||||
|
*
|
||||||
|
* @param defaultFormat the default format to look for
|
||||||
|
* @param limitDataUsage choose low bitrate audio stream
|
||||||
|
* @return Comparator
|
||||||
|
*/
|
||||||
|
static Comparator<AudioStream> getAudioFormatComparator(
|
||||||
|
@Nullable final MediaFormat defaultFormat, final boolean limitDataUsage) {
|
||||||
|
final List<MediaFormat> formatRanking = limitDataUsage
|
||||||
|
? AUDIO_FORMAT_EFFICIENCY_RANKING : AUDIO_FORMAT_QUALITY_RANKING;
|
||||||
|
|
||||||
|
Comparator<AudioStream> bitrateComparator =
|
||||||
|
Comparator.comparingInt(AudioStream::getAverageBitrate);
|
||||||
|
if (limitDataUsage) {
|
||||||
|
bitrateComparator = bitrateComparator.reversed();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Comparator.comparing(AudioStream::getFormat, (o1, o2) -> {
|
||||||
|
if (defaultFormat != null) {
|
||||||
|
return Boolean.compare(o1 == defaultFormat, o2 == defaultFormat);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}).thenComparing(bitrateComparator).thenComparingInt(
|
||||||
|
stream -> formatRanking.indexOf(stream.getFormat()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a {@link Comparator} to compare {@link AudioStream}s by their tracks.
|
||||||
|
*
|
||||||
|
* <p>Tracks will be compared this order:</p>
|
||||||
|
* <ol>
|
||||||
|
* <li>If {@code preferOriginalAudio}: use original audio</li>
|
||||||
|
* <li>Language matches {@code preferredLanguage}</li>
|
||||||
|
* <li>
|
||||||
|
* Track type ranks highest in this order:
|
||||||
|
* <i>Original</i> > <i>Dubbed</i> > <i>Descriptive</i>
|
||||||
|
* <p>If {@code preferDescriptiveAudio}:
|
||||||
|
* <i>Descriptive</i> > <i>Dubbed</i> > <i>Original</i></p>
|
||||||
|
* </li>
|
||||||
|
* <li>Language is English</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* <p>The prefered track will be ordered last.</p>
|
||||||
|
*
|
||||||
|
* @param context App context
|
||||||
|
* @return Comparator
|
||||||
|
*/
|
||||||
|
private static Comparator<AudioStream> getAudioTrackComparator(
|
||||||
|
@NonNull final Context context) {
|
||||||
|
final SharedPreferences preferences =
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
|
final Locale preferredLanguage = Localization.getPreferredLocale(context);
|
||||||
|
final boolean preferOriginalAudio =
|
||||||
|
preferences.getBoolean(context.getString(R.string.prefer_original_audio_key),
|
||||||
|
false);
|
||||||
|
final boolean preferDescriptiveAudio =
|
||||||
|
preferences.getBoolean(context.getString(R.string.prefer_descriptive_audio_key),
|
||||||
|
false);
|
||||||
|
|
||||||
|
return getAudioTrackComparator(preferredLanguage, preferOriginalAudio,
|
||||||
|
preferDescriptiveAudio);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a {@link Comparator} to compare {@link AudioStream}s by their tracks.
|
||||||
|
*
|
||||||
|
* <p>Tracks will be compared this order:</p>
|
||||||
|
* <ol>
|
||||||
|
* <li>If {@code preferOriginalAudio}: use original audio</li>
|
||||||
|
* <li>Language matches {@code preferredLanguage}</li>
|
||||||
|
* <li>
|
||||||
|
* Track type ranks highest in this order:
|
||||||
|
* <i>Original</i> > <i>Dubbed</i> > <i>Descriptive</i>
|
||||||
|
* <p>If {@code preferDescriptiveAudio}:
|
||||||
|
* <i>Descriptive</i> > <i>Dubbed</i> > <i>Original</i></p>
|
||||||
|
* </li>
|
||||||
|
* <li>Language is English</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* <p>The prefered track will be ordered last.</p>
|
||||||
|
*
|
||||||
|
* @param preferredLanguage Preferred audio stream language
|
||||||
|
* @param preferOriginalAudio Get the original audio track regardless of its language
|
||||||
|
* @param preferDescriptiveAudio Prefer the descriptive audio track if available
|
||||||
|
* @return Comparator
|
||||||
|
*/
|
||||||
|
static Comparator<AudioStream> getAudioTrackComparator(
|
||||||
|
final Locale preferredLanguage,
|
||||||
|
final boolean preferOriginalAudio,
|
||||||
|
final boolean preferDescriptiveAudio) {
|
||||||
|
final String langCode = preferredLanguage.getISO3Language();
|
||||||
|
final List<AudioTrackType> trackTypeRanking = preferDescriptiveAudio
|
||||||
|
? AUDIO_TRACK_TYPE_RANKING_DESCRIPTIVE : AUDIO_TRACK_TYPE_RANKING;
|
||||||
|
|
||||||
|
return Comparator.comparing(AudioStream::getAudioTrackType, (o1, o2) -> {
|
||||||
|
if (preferOriginalAudio) {
|
||||||
|
return Boolean.compare(
|
||||||
|
o1 == AudioTrackType.ORIGINAL, o2 == AudioTrackType.ORIGINAL);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}).thenComparing(AudioStream::getAudioLocale,
|
||||||
|
Comparator.nullsFirst(Comparator.comparing(
|
||||||
|
locale -> locale.getISO3Language().equals(langCode))))
|
||||||
|
.thenComparing(AudioStream::getAudioTrackType,
|
||||||
|
Comparator.nullsFirst(Comparator.comparingInt(trackTypeRanking::indexOf)))
|
||||||
|
.thenComparing(AudioStream::getAudioLocale,
|
||||||
|
Comparator.nullsFirst(Comparator.comparing(
|
||||||
|
locale -> locale.getISO3Language().equals(
|
||||||
|
Locale.ENGLISH.getISO3Language()))));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a {@link Comparator} to compare {@link AudioStream}s by their languages and track types
|
||||||
|
* for alphabetical sorting.
|
||||||
|
*
|
||||||
|
* @param context app context for localization
|
||||||
|
* @return Comparator
|
||||||
|
*/
|
||||||
|
private static Comparator<AudioStream> getAudioTrackNameComparator(
|
||||||
|
@NonNull final Context context) {
|
||||||
|
final Locale appLoc = Localization.getAppLocale(context);
|
||||||
|
|
||||||
|
return Comparator.comparing(AudioStream::getAudioLocale, Comparator.nullsLast(
|
||||||
|
Comparator.comparing(locale -> locale.getDisplayName(appLoc))))
|
||||||
|
.thenComparing(AudioStream::getAudioTrackType);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import android.text.TextUtils;
|
|||||||
import android.util.DisplayMetrics;
|
import android.util.DisplayMetrics;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.PluralsRes;
|
import androidx.annotation.PluralsRes;
|
||||||
import androidx.annotation.StringRes;
|
import androidx.annotation.StringRes;
|
||||||
import androidx.core.math.MathUtils;
|
import androidx.core.math.MathUtils;
|
||||||
@ -21,6 +22,8 @@ import org.ocpsoft.prettytime.units.Decade;
|
|||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.extractor.ListExtractor;
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
import org.schabi.newpipe.extractor.localization.ContentCountry;
|
import org.schabi.newpipe.extractor.localization.ContentCountry;
|
||||||
|
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||||
|
import org.schabi.newpipe.extractor.stream.AudioTrackType;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.RoundingMode;
|
import java.math.RoundingMode;
|
||||||
@ -261,6 +264,52 @@ public final class Localization {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the localized name of an audio track.
|
||||||
|
*
|
||||||
|
* <p>Examples of results returned by this method:</p>
|
||||||
|
* <ul>
|
||||||
|
* <li>English (original)</li>
|
||||||
|
* <li>English (descriptive)</li>
|
||||||
|
* <li>Spanish (dubbed)</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @param context the context used to get the app language
|
||||||
|
* @param track an {@link AudioStream} of the track
|
||||||
|
* @return the localized name of the audio track
|
||||||
|
*/
|
||||||
|
public static String audioTrackName(final Context context, final AudioStream track) {
|
||||||
|
final String name;
|
||||||
|
if (track.getAudioLocale() != null) {
|
||||||
|
name = track.getAudioLocale().getDisplayLanguage(getAppLocale(context));
|
||||||
|
} else if (track.getAudioTrackName() != null) {
|
||||||
|
name = track.getAudioTrackName();
|
||||||
|
} else {
|
||||||
|
name = context.getString(R.string.unknown_audio_track);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (track.getAudioTrackType() != null) {
|
||||||
|
final String trackType = audioTrackType(context, track.getAudioTrackType());
|
||||||
|
if (trackType != null) {
|
||||||
|
return context.getString(R.string.audio_track_name, name, trackType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static String audioTrackType(final Context context, final AudioTrackType trackType) {
|
||||||
|
switch (trackType) {
|
||||||
|
case ORIGINAL:
|
||||||
|
return context.getString(R.string.audio_track_type_original);
|
||||||
|
case DUBBED:
|
||||||
|
return context.getString(R.string.audio_track_type_dubbed);
|
||||||
|
case DESCRIPTIVE:
|
||||||
|
return context.getString(R.string.audio_track_type_descriptive);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Pretty Time
|
// Pretty Time
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
@ -224,6 +224,8 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
|
|||||||
public static class StreamSizeWrapper<T extends Stream> implements Serializable {
|
public static class StreamSizeWrapper<T extends Stream> implements Serializable {
|
||||||
private static final StreamSizeWrapper<Stream> EMPTY =
|
private static final StreamSizeWrapper<Stream> EMPTY =
|
||||||
new StreamSizeWrapper<>(Collections.emptyList(), null);
|
new StreamSizeWrapper<>(Collections.emptyList(), null);
|
||||||
|
private static final int SIZE_UNSET = -2;
|
||||||
|
|
||||||
private final List<T> streamsList;
|
private final List<T> streamsList;
|
||||||
private final long[] streamSizes;
|
private final long[] streamSizes;
|
||||||
private final String unknownSize;
|
private final String unknownSize;
|
||||||
@ -235,7 +237,7 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
|
|||||||
this.unknownSize = context == null
|
this.unknownSize = context == null
|
||||||
? "--.-" : context.getString(R.string.unknown_content);
|
? "--.-" : context.getString(R.string.unknown_content);
|
||||||
|
|
||||||
Arrays.fill(streamSizes, -2);
|
resetSizes();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -251,7 +253,7 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
|
|||||||
final Callable<Boolean> fetchAndSet = () -> {
|
final Callable<Boolean> fetchAndSet = () -> {
|
||||||
boolean hasChanged = false;
|
boolean hasChanged = false;
|
||||||
for (final X stream : streamsWrapper.getStreamsList()) {
|
for (final X stream : streamsWrapper.getStreamsList()) {
|
||||||
if (streamsWrapper.getSizeInBytes(stream) > -2) {
|
if (streamsWrapper.getSizeInBytes(stream) > SIZE_UNSET) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -269,6 +271,10 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
|
|||||||
.onErrorReturnItem(true);
|
.onErrorReturnItem(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void resetSizes() {
|
||||||
|
Arrays.fill(streamSizes, SIZE_UNSET);
|
||||||
|
}
|
||||||
|
|
||||||
public static <X extends Stream> StreamSizeWrapper<X> empty() {
|
public static <X extends Stream> StreamSizeWrapper<X> empty() {
|
||||||
//noinspection unchecked
|
//noinspection unchecked
|
||||||
return (StreamSizeWrapper<X>) EMPTY;
|
return (StreamSizeWrapper<X>) EMPTY;
|
||||||
|
@ -71,11 +71,45 @@
|
|||||||
android:minWidth="150dp"
|
android:minWidth="150dp"
|
||||||
tools:listitem="@layout/stream_quality_item" />
|
tools:listitem="@layout/stream_quality_item" />
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/audio_track_spinner"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_below="@+id/quality_spinner"
|
||||||
|
android:layout_marginLeft="20dp"
|
||||||
|
android:layout_marginRight="20dp"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
android:minWidth="150dp"
|
||||||
|
tools:visibility="gone" />
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/audio_stream_spinner"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_below="@+id/audio_track_spinner"
|
||||||
|
android:layout_marginLeft="20dp"
|
||||||
|
android:layout_marginRight="20dp"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
android:minWidth="150dp"
|
||||||
|
tools:visibility="gone" />
|
||||||
|
|
||||||
|
<org.schabi.newpipe.views.NewPipeTextView
|
||||||
|
android:id="@+id/audio_track_present_in_video_text"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_below="@+id/audio_stream_spinner"
|
||||||
|
android:layout_marginLeft="24dp"
|
||||||
|
android:layout_marginRight="24dp"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/audio_track_present_in_video"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
<org.schabi.newpipe.views.NewPipeTextView
|
<org.schabi.newpipe.views.NewPipeTextView
|
||||||
android:id="@+id/threads_text_view"
|
android:id="@+id/threads_text_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_below="@+id/quality_spinner"
|
android:layout_below="@+id/audio_track_present_in_video_text"
|
||||||
android:layout_marginLeft="24dp"
|
android:layout_marginLeft="24dp"
|
||||||
android:layout_marginRight="24dp"
|
android:layout_marginRight="24dp"
|
||||||
android:layout_marginBottom="6dp"
|
android:layout_marginBottom="6dp"
|
||||||
|
@ -157,6 +157,22 @@
|
|||||||
tools:text="The Video Artist LONG very LONG very Long" />
|
tools:text="The Video Artist LONG very LONG very Long" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<org.schabi.newpipe.views.NewPipeTextView
|
||||||
|
android:id="@+id/audioTrackTextView"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="35dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:gravity="center"
|
||||||
|
android:minWidth="0dp"
|
||||||
|
android:padding="@dimen/player_main_buttons_padding"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:ignore="HardcodedText,RtlHardcoded"
|
||||||
|
tools:visibility="visible"
|
||||||
|
tools:text="English (Original)" />
|
||||||
|
|
||||||
<org.schabi.newpipe.views.NewPipeTextView
|
<org.schabi.newpipe.views.NewPipeTextView
|
||||||
android:id="@+id/qualityTextView"
|
android:id="@+id/qualityTextView"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
@ -18,6 +18,14 @@
|
|||||||
android:visible="true"
|
android:visible="true"
|
||||||
app:showAsAction="ifRoom" />
|
app:showAsAction="ifRoom" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_audio_track"
|
||||||
|
android:tooltipText="@string/audio_track"
|
||||||
|
android:visible="false"
|
||||||
|
app:showAsAction="ifRoom">
|
||||||
|
<menu />
|
||||||
|
</item>
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_mute"
|
android:id="@+id/action_mute"
|
||||||
android:icon="@drawable/ic_volume_off"
|
android:icon="@drawable/ic_volume_off"
|
||||||
|
@ -219,6 +219,8 @@
|
|||||||
<item>@string/none_control_key</item>
|
<item>@string/none_control_key</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<string name="prefer_original_audio_key">prefer_original_audio</string>
|
||||||
|
<string name="prefer_descriptive_audio_key">prefer_descriptive_audio</string>
|
||||||
<string name="last_resize_mode">last_resize_mode</string>
|
<string name="last_resize_mode">last_resize_mode</string>
|
||||||
|
|
||||||
<!-- DEBUG ONLY -->
|
<!-- DEBUG ONLY -->
|
||||||
|
@ -94,6 +94,10 @@
|
|||||||
<string name="show_description_summary">Turn off to hide video description and additional information</string>
|
<string name="show_description_summary">Turn off to hide video description and additional information</string>
|
||||||
<string name="show_meta_info_title">Show meta info</string>
|
<string name="show_meta_info_title">Show meta info</string>
|
||||||
<string name="show_meta_info_summary">Turn off to hide meta info boxes with additional information about the stream creator, stream content or a search request</string>
|
<string name="show_meta_info_summary">Turn off to hide meta info boxes with additional information about the stream creator, stream content or a search request</string>
|
||||||
|
<string name="prefer_original_audio_title">Prefer original audio</string>
|
||||||
|
<string name="prefer_original_audio_summary">Select the original audio track regardless of the language</string>
|
||||||
|
<string name="prefer_descriptive_audio_title">Prefer descriptive audio</string>
|
||||||
|
<string name="prefer_descriptive_audio_summary">Select an audio track with descriptions for visually impaired people if available</string>
|
||||||
<string name="thumbnail_cache_wipe_complete_notice">Image cache wiped</string>
|
<string name="thumbnail_cache_wipe_complete_notice">Image cache wiped</string>
|
||||||
<string name="metadata_cache_wipe_title">Wipe cached metadata</string>
|
<string name="metadata_cache_wipe_title">Wipe cached metadata</string>
|
||||||
<string name="metadata_cache_wipe_summary">Remove all cached webpage data</string>
|
<string name="metadata_cache_wipe_summary">Remove all cached webpage data</string>
|
||||||
@ -414,6 +418,8 @@
|
|||||||
<string name="play_queue_remove">Remove</string>
|
<string name="play_queue_remove">Remove</string>
|
||||||
<string name="play_queue_stream_detail">Details</string>
|
<string name="play_queue_stream_detail">Details</string>
|
||||||
<string name="play_queue_audio_settings">Audio Settings</string>
|
<string name="play_queue_audio_settings">Audio Settings</string>
|
||||||
|
<string name="play_queue_audio_track">Audio: %s</string>
|
||||||
|
<string name="audio_track">Audio track</string>
|
||||||
<string name="hold_to_append">Hold to enqueue</string>
|
<string name="hold_to_append">Hold to enqueue</string>
|
||||||
<string name="show_channel_details">Show channel details</string>
|
<string name="show_channel_details">Show channel details</string>
|
||||||
<string name="enqueue_stream">Enqueue</string>
|
<string name="enqueue_stream">Enqueue</string>
|
||||||
@ -761,12 +767,15 @@
|
|||||||
<string name="enumeration_comma">,</string>
|
<string name="enumeration_comma">,</string>
|
||||||
<string name="toggle_all">Toggle all</string>
|
<string name="toggle_all">Toggle all</string>
|
||||||
<string name="streams_not_yet_supported_removed">Streams which are not yet supported by the downloader are not shown</string>
|
<string name="streams_not_yet_supported_removed">Streams which are not yet supported by the downloader are not shown</string>
|
||||||
|
<string name="audio_track_present_in_video">An audio track should be already present in this stream</string>
|
||||||
<string name="selected_stream_external_player_not_supported">The selected stream is not supported by external players</string>
|
<string name="selected_stream_external_player_not_supported">The selected stream is not supported by external players</string>
|
||||||
<string name="no_audio_streams_available_for_external_players">No audio streams are available for external players</string>
|
<string name="no_audio_streams_available_for_external_players">No audio streams are available for external players</string>
|
||||||
<string name="no_video_streams_available_for_external_players">No video streams are available for external players</string>
|
<string name="no_video_streams_available_for_external_players">No video streams are available for external players</string>
|
||||||
<string name="select_quality_external_players">Select quality for external players</string>
|
<string name="select_quality_external_players">Select quality for external players</string>
|
||||||
|
<string name="select_audio_track_external_players">Select audio track for external players</string>
|
||||||
<string name="unknown_format">Unknown format</string>
|
<string name="unknown_format">Unknown format</string>
|
||||||
<string name="unknown_quality">Unknown quality</string>
|
<string name="unknown_quality">Unknown quality</string>
|
||||||
|
<string name="unknown_audio_track">Unknown</string>
|
||||||
<string name="feed_toggle_show_future_items">Show future items</string>
|
<string name="feed_toggle_show_future_items">Show future items</string>
|
||||||
<string name="feed_toggle_hide_future_items">Hide future items</string>
|
<string name="feed_toggle_hide_future_items">Hide future items</string>
|
||||||
<string name="feed_show_watched">Fully watched</string>
|
<string name="feed_show_watched">Fully watched</string>
|
||||||
@ -779,4 +788,8 @@
|
|||||||
<string name="use_exoplayer_decoder_fallback_summary">Enable this option if you have decoder initialization issues, which falls back to lower-priority decoders if primary decoders initialization fail. This may result in poor playback performance than when using primary decoders</string>
|
<string name="use_exoplayer_decoder_fallback_summary">Enable this option if you have decoder initialization issues, which falls back to lower-priority decoders if primary decoders initialization fail. This may result in poor playback performance than when using primary decoders</string>
|
||||||
<string name="always_use_exoplayer_set_output_surface_workaround_title">Always use ExoPlayer\'s video output surface setting workaround</string>
|
<string name="always_use_exoplayer_set_output_surface_workaround_title">Always use ExoPlayer\'s video output surface setting workaround</string>
|
||||||
<string name="always_use_exoplayer_set_output_surface_workaround_summary">This workaround releases and re-instantiates video codecs when a surface change occurs, instead of setting the surface to the codec directly. Already used by ExoPlayer on some devices with this issue, this setting has only an effect on Android 6 and higher\n\nEnabling this option may prevent playback errors when switching the current video player or switching to fullscreen</string>
|
<string name="always_use_exoplayer_set_output_surface_workaround_summary">This workaround releases and re-instantiates video codecs when a surface change occurs, instead of setting the surface to the codec directly. Already used by ExoPlayer on some devices with this issue, this setting has only an effect on Android 6 and higher\n\nEnabling this option may prevent playback errors when switching the current video player or switching to fullscreen</string>
|
||||||
|
<string name="audio_track_name">%s %s</string>
|
||||||
|
<string name="audio_track_type_original">original</string>
|
||||||
|
<string name="audio_track_type_dubbed">dubbed</string>
|
||||||
|
<string name="audio_track_type_descriptive">descriptive</string>
|
||||||
</resources>
|
</resources>
|
@ -61,6 +61,22 @@
|
|||||||
app:iconSpaceReserved="false"
|
app:iconSpaceReserved="false"
|
||||||
app:useSimpleSummaryProvider="true" />
|
app:useSimpleSummaryProvider="true" />
|
||||||
|
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
android:defaultValue="false"
|
||||||
|
android:key="@string/prefer_original_audio_key"
|
||||||
|
android:summary="@string/prefer_original_audio_summary"
|
||||||
|
android:title="@string/prefer_original_audio_title"
|
||||||
|
app:singleLineTitle="false"
|
||||||
|
app:iconSpaceReserved="false"/>
|
||||||
|
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
android:defaultValue="false"
|
||||||
|
android:key="@string/prefer_descriptive_audio_key"
|
||||||
|
android:summary="@string/prefer_descriptive_audio_summary"
|
||||||
|
android:title="@string/prefer_descriptive_audio_title"
|
||||||
|
app:singleLineTitle="false"
|
||||||
|
app:iconSpaceReserved="false"/>
|
||||||
|
|
||||||
<PreferenceScreen
|
<PreferenceScreen
|
||||||
android:fragment="org.schabi.newpipe.settings.ExoPlayerSettingsFragment"
|
android:fragment="org.schabi.newpipe.settings.ExoPlayerSettingsFragment"
|
||||||
android:key="@string/exoplayer_settings_key"
|
android:key="@string/exoplayer_settings_key"
|
||||||
|
@ -3,10 +3,13 @@ package org.schabi.newpipe.util;
|
|||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.schabi.newpipe.extractor.MediaFormat;
|
import org.schabi.newpipe.extractor.MediaFormat;
|
||||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||||
|
import org.schabi.newpipe.extractor.stream.AudioTrackType;
|
||||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
@ -29,6 +32,15 @@ public class ListHelperTest {
|
|||||||
generateAudioStream("mp3-192", MediaFormat.MP3, 192),
|
generateAudioStream("mp3-192", MediaFormat.MP3, 192),
|
||||||
generateAudioStream("webma-320", MediaFormat.WEBMA, 320));
|
generateAudioStream("webma-320", MediaFormat.WEBMA, 320));
|
||||||
|
|
||||||
|
private static final List<AudioStream> AUDIO_TRACKS_TEST_LIST = List.of(
|
||||||
|
generateAudioTrack("en.or", "en.or", Locale.ENGLISH, AudioTrackType.ORIGINAL),
|
||||||
|
generateAudioTrack("en.du", "en.du", Locale.ENGLISH, AudioTrackType.DUBBED),
|
||||||
|
generateAudioTrack("en.ds", "en.ds", Locale.ENGLISH, AudioTrackType.DESCRIPTIVE),
|
||||||
|
generateAudioTrack("unknown", null, null, null),
|
||||||
|
generateAudioTrack("de.du", "de.du", Locale.GERMAN, AudioTrackType.DUBBED),
|
||||||
|
generateAudioTrack("de.ds", "de.ds", Locale.GERMAN, AudioTrackType.DESCRIPTIVE)
|
||||||
|
);
|
||||||
|
|
||||||
private static final List<VideoStream> VIDEO_STREAMS_TEST_LIST = List.of(
|
private static final List<VideoStream> VIDEO_STREAMS_TEST_LIST = List.of(
|
||||||
generateVideoStream("mpeg_4-720", MediaFormat.MPEG_4, "720p", false),
|
generateVideoStream("mpeg_4-720", MediaFormat.MPEG_4, "720p", false),
|
||||||
generateVideoStream("v3gpp-240", MediaFormat.v3GPP, "240p", false),
|
generateVideoStream("v3gpp-240", MediaFormat.v3GPP, "240p", false),
|
||||||
@ -199,24 +211,29 @@ public class ListHelperTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getHighestQualityAudioFormatTest() {
|
public void getHighestQualityAudioFormatTest() {
|
||||||
AudioStream stream = AUDIO_STREAMS_TEST_LIST.get(ListHelper.getHighestQualityAudioIndex(
|
Comparator<AudioStream> cmp = ListHelper.getAudioFormatComparator(MediaFormat.M4A, false);
|
||||||
MediaFormat.M4A, AUDIO_STREAMS_TEST_LIST));
|
AudioStream stream = AUDIO_STREAMS_TEST_LIST.get(ListHelper.getAudioIndexByHighestRank(
|
||||||
|
AUDIO_STREAMS_TEST_LIST, cmp));
|
||||||
assertEquals(320, stream.getAverageBitrate());
|
assertEquals(320, stream.getAverageBitrate());
|
||||||
assertEquals(MediaFormat.M4A, stream.getFormat());
|
assertEquals(MediaFormat.M4A, stream.getFormat());
|
||||||
|
|
||||||
stream = AUDIO_STREAMS_TEST_LIST.get(ListHelper.getHighestQualityAudioIndex(
|
cmp = ListHelper.getAudioFormatComparator(MediaFormat.WEBMA, false);
|
||||||
MediaFormat.WEBMA, AUDIO_STREAMS_TEST_LIST));
|
stream = AUDIO_STREAMS_TEST_LIST.get(ListHelper.getAudioIndexByHighestRank(
|
||||||
|
AUDIO_STREAMS_TEST_LIST, cmp));
|
||||||
assertEquals(320, stream.getAverageBitrate());
|
assertEquals(320, stream.getAverageBitrate());
|
||||||
assertEquals(MediaFormat.WEBMA, stream.getFormat());
|
assertEquals(MediaFormat.WEBMA, stream.getFormat());
|
||||||
|
|
||||||
stream = AUDIO_STREAMS_TEST_LIST.get(ListHelper.getHighestQualityAudioIndex(
|
cmp = ListHelper.getAudioFormatComparator(MediaFormat.MP3, false);
|
||||||
MediaFormat.MP3, AUDIO_STREAMS_TEST_LIST));
|
stream = AUDIO_STREAMS_TEST_LIST.get(ListHelper.getAudioIndexByHighestRank(
|
||||||
|
AUDIO_STREAMS_TEST_LIST, cmp));
|
||||||
assertEquals(192, stream.getAverageBitrate());
|
assertEquals(192, stream.getAverageBitrate());
|
||||||
assertEquals(MediaFormat.MP3, stream.getFormat());
|
assertEquals(MediaFormat.MP3, stream.getFormat());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getHighestQualityAudioFormatPreferredAbsent() {
|
public void getHighestQualityAudioFormatPreferredAbsent() {
|
||||||
|
final Comparator<AudioStream> cmp =
|
||||||
|
ListHelper.getAudioFormatComparator(MediaFormat.MP3, false);
|
||||||
|
|
||||||
//////////////////////////////////////////
|
//////////////////////////////////////////
|
||||||
// Doesn't contain the preferred format //
|
// Doesn't contain the preferred format //
|
||||||
@ -227,8 +244,7 @@ public class ListHelperTest {
|
|||||||
generateAudioStream("webma-192", MediaFormat.WEBMA, 192));
|
generateAudioStream("webma-192", MediaFormat.WEBMA, 192));
|
||||||
// List doesn't contains this format
|
// List doesn't contains this format
|
||||||
// It should fallback to the highest bitrate audio no matter what format it is
|
// It should fallback to the highest bitrate audio no matter what format it is
|
||||||
AudioStream stream = testList.get(ListHelper.getHighestQualityAudioIndex(
|
AudioStream stream = testList.get(ListHelper.getAudioIndexByHighestRank(testList, cmp));
|
||||||
MediaFormat.MP3, testList));
|
|
||||||
assertEquals(192, stream.getAverageBitrate());
|
assertEquals(192, stream.getAverageBitrate());
|
||||||
assertEquals(MediaFormat.WEBMA, stream.getFormat());
|
assertEquals(MediaFormat.WEBMA, stream.getFormat());
|
||||||
|
|
||||||
@ -246,44 +262,51 @@ public class ListHelperTest {
|
|||||||
generateAudioStream("webma-192-4", MediaFormat.WEBMA, 192)));
|
generateAudioStream("webma-192-4", MediaFormat.WEBMA, 192)));
|
||||||
// List doesn't contains this format, it should fallback to the highest bitrate audio and
|
// List doesn't contains this format, it should fallback to the highest bitrate audio and
|
||||||
// the highest quality format.
|
// the highest quality format.
|
||||||
stream = testList.get(ListHelper.getHighestQualityAudioIndex(MediaFormat.MP3, testList));
|
stream =
|
||||||
|
testList.get(ListHelper.getAudioIndexByHighestRank(testList, cmp));
|
||||||
assertEquals(192, stream.getAverageBitrate());
|
assertEquals(192, stream.getAverageBitrate());
|
||||||
assertEquals(MediaFormat.M4A, stream.getFormat());
|
assertEquals(MediaFormat.M4A, stream.getFormat());
|
||||||
|
|
||||||
// Adding a new format and bitrate. Adding another stream will have no impact since
|
// Adding a new format and bitrate. Adding another stream will have no impact since
|
||||||
// it's not a preferred format.
|
// it's not a preferred format.
|
||||||
testList.add(generateAudioStream("webma-192-5", MediaFormat.WEBMA, 192));
|
testList.add(generateAudioStream("webma-192-5", MediaFormat.WEBMA, 192));
|
||||||
stream = testList.get(ListHelper.getHighestQualityAudioIndex(MediaFormat.MP3, testList));
|
stream =
|
||||||
|
testList.get(ListHelper.getAudioIndexByHighestRank(testList, cmp));
|
||||||
assertEquals(192, stream.getAverageBitrate());
|
assertEquals(192, stream.getAverageBitrate());
|
||||||
assertEquals(MediaFormat.M4A, stream.getFormat());
|
assertEquals(MediaFormat.M4A, stream.getFormat());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getHighestQualityAudioNull() {
|
public void getHighestQualityAudioNull() {
|
||||||
assertEquals(-1, ListHelper.getHighestQualityAudioIndex(null, null));
|
final Comparator<AudioStream> cmp = ListHelper.getAudioFormatComparator(null, false);
|
||||||
assertEquals(-1, ListHelper.getHighestQualityAudioIndex(null, new ArrayList<>()));
|
assertEquals(-1, ListHelper.getAudioIndexByHighestRank(null, cmp));
|
||||||
|
assertEquals(-1, ListHelper.getAudioIndexByHighestRank(new ArrayList<>(), cmp));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getLowestQualityAudioFormatTest() {
|
public void getLowestQualityAudioFormatTest() {
|
||||||
AudioStream stream = AUDIO_STREAMS_TEST_LIST.get(ListHelper.getMostCompactAudioIndex(
|
Comparator<AudioStream> cmp = ListHelper.getAudioFormatComparator(MediaFormat.M4A, true);
|
||||||
MediaFormat.M4A, AUDIO_STREAMS_TEST_LIST));
|
AudioStream stream = AUDIO_STREAMS_TEST_LIST.get(ListHelper.getAudioIndexByHighestRank(
|
||||||
|
AUDIO_STREAMS_TEST_LIST, cmp));
|
||||||
assertEquals(128, stream.getAverageBitrate());
|
assertEquals(128, stream.getAverageBitrate());
|
||||||
assertEquals(MediaFormat.M4A, stream.getFormat());
|
assertEquals(MediaFormat.M4A, stream.getFormat());
|
||||||
|
|
||||||
stream = AUDIO_STREAMS_TEST_LIST.get(ListHelper.getMostCompactAudioIndex(
|
cmp = ListHelper.getAudioFormatComparator(MediaFormat.WEBMA, true);
|
||||||
MediaFormat.WEBMA, AUDIO_STREAMS_TEST_LIST));
|
stream = AUDIO_STREAMS_TEST_LIST.get(ListHelper.getAudioIndexByHighestRank(
|
||||||
|
AUDIO_STREAMS_TEST_LIST, cmp));
|
||||||
assertEquals(64, stream.getAverageBitrate());
|
assertEquals(64, stream.getAverageBitrate());
|
||||||
assertEquals(MediaFormat.WEBMA, stream.getFormat());
|
assertEquals(MediaFormat.WEBMA, stream.getFormat());
|
||||||
|
|
||||||
stream = AUDIO_STREAMS_TEST_LIST.get(ListHelper.getMostCompactAudioIndex(
|
cmp = ListHelper.getAudioFormatComparator(MediaFormat.MP3, true);
|
||||||
MediaFormat.MP3, AUDIO_STREAMS_TEST_LIST));
|
stream = AUDIO_STREAMS_TEST_LIST.get(ListHelper.getAudioIndexByHighestRank(
|
||||||
|
AUDIO_STREAMS_TEST_LIST, cmp));
|
||||||
assertEquals(64, stream.getAverageBitrate());
|
assertEquals(64, stream.getAverageBitrate());
|
||||||
assertEquals(MediaFormat.MP3, stream.getFormat());
|
assertEquals(MediaFormat.MP3, stream.getFormat());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getLowestQualityAudioFormatPreferredAbsent() {
|
public void getLowestQualityAudioFormatPreferredAbsent() {
|
||||||
|
Comparator<AudioStream> cmp = ListHelper.getAudioFormatComparator(MediaFormat.MP3, true);
|
||||||
|
|
||||||
//////////////////////////////////////////
|
//////////////////////////////////////////
|
||||||
// Doesn't contain the preferred format //
|
// Doesn't contain the preferred format //
|
||||||
@ -294,14 +317,13 @@ public class ListHelperTest {
|
|||||||
generateAudioStream("webma-192-1", MediaFormat.WEBMA, 192)));
|
generateAudioStream("webma-192-1", MediaFormat.WEBMA, 192)));
|
||||||
// List doesn't contains this format
|
// List doesn't contains this format
|
||||||
// It should fallback to the most compact audio no matter what format it is.
|
// It should fallback to the most compact audio no matter what format it is.
|
||||||
AudioStream stream = testList.get(ListHelper.getMostCompactAudioIndex(
|
AudioStream stream = testList.get(ListHelper.getAudioIndexByHighestRank(testList, cmp));
|
||||||
MediaFormat.MP3, testList));
|
|
||||||
assertEquals(128, stream.getAverageBitrate());
|
assertEquals(128, stream.getAverageBitrate());
|
||||||
assertEquals(MediaFormat.M4A, stream.getFormat());
|
assertEquals(MediaFormat.M4A, stream.getFormat());
|
||||||
|
|
||||||
// WEBMA is more compact than M4A
|
// WEBMA is more compact than M4A
|
||||||
testList.add(generateAudioStream("webma-192-2", MediaFormat.WEBMA, 128));
|
testList.add(generateAudioStream("webma-192-2", MediaFormat.WEBMA, 128));
|
||||||
stream = testList.get(ListHelper.getMostCompactAudioIndex(MediaFormat.MP3, testList));
|
stream = testList.get(ListHelper.getAudioIndexByHighestRank(testList, cmp));
|
||||||
assertEquals(128, stream.getAverageBitrate());
|
assertEquals(128, stream.getAverageBitrate());
|
||||||
assertEquals(MediaFormat.WEBMA, stream.getFormat());
|
assertEquals(MediaFormat.WEBMA, stream.getFormat());
|
||||||
|
|
||||||
@ -318,20 +340,58 @@ public class ListHelperTest {
|
|||||||
generateAudioStream("m4a-192-3", MediaFormat.M4A, 192)));
|
generateAudioStream("m4a-192-3", MediaFormat.M4A, 192)));
|
||||||
// List doesn't contain this format
|
// List doesn't contain this format
|
||||||
// It should fallback to the most compact audio no matter what format it is.
|
// It should fallback to the most compact audio no matter what format it is.
|
||||||
stream = testList.get(ListHelper.getMostCompactAudioIndex(MediaFormat.MP3, testList));
|
stream = testList.get(
|
||||||
|
ListHelper.getAudioIndexByHighestRank(testList, cmp));
|
||||||
assertEquals(192, stream.getAverageBitrate());
|
assertEquals(192, stream.getAverageBitrate());
|
||||||
assertEquals(MediaFormat.WEBMA, stream.getFormat());
|
assertEquals(MediaFormat.WEBMA, stream.getFormat());
|
||||||
|
|
||||||
// Should be same as above
|
// Should be same as above
|
||||||
stream = testList.get(ListHelper.getMostCompactAudioIndex(null, testList));
|
cmp = ListHelper.getAudioFormatComparator(null, true);
|
||||||
|
stream = testList.get(
|
||||||
|
ListHelper.getAudioIndexByHighestRank(testList, cmp));
|
||||||
assertEquals(192, stream.getAverageBitrate());
|
assertEquals(192, stream.getAverageBitrate());
|
||||||
assertEquals(MediaFormat.WEBMA, stream.getFormat());
|
assertEquals(MediaFormat.WEBMA, stream.getFormat());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getLowestQualityAudioNull() {
|
public void getLowestQualityAudioNull() {
|
||||||
assertEquals(-1, ListHelper.getMostCompactAudioIndex(null, null));
|
final Comparator<AudioStream> cmp = ListHelper.getAudioFormatComparator(null, false);
|
||||||
assertEquals(-1, ListHelper.getMostCompactAudioIndex(null, new ArrayList<>()));
|
assertEquals(-1, ListHelper.getAudioIndexByHighestRank(null, cmp));
|
||||||
|
assertEquals(-1, ListHelper.getAudioIndexByHighestRank(new ArrayList<>(), cmp));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getAudioTrack() {
|
||||||
|
// English language
|
||||||
|
Comparator<AudioStream> cmp =
|
||||||
|
ListHelper.getAudioTrackComparator(Locale.ENGLISH, false, false);
|
||||||
|
AudioStream stream = AUDIO_TRACKS_TEST_LIST.get(ListHelper.getAudioIndexByHighestRank(
|
||||||
|
AUDIO_TRACKS_TEST_LIST, cmp));
|
||||||
|
assertEquals("en.or", stream.getId());
|
||||||
|
|
||||||
|
// German language
|
||||||
|
cmp = ListHelper.getAudioTrackComparator(Locale.GERMAN, false, false);
|
||||||
|
stream = AUDIO_TRACKS_TEST_LIST.get(ListHelper.getAudioIndexByHighestRank(
|
||||||
|
AUDIO_TRACKS_TEST_LIST, cmp));
|
||||||
|
assertEquals("de.du", stream.getId());
|
||||||
|
|
||||||
|
// German language, but prefer original
|
||||||
|
cmp = ListHelper.getAudioTrackComparator(Locale.GERMAN, true, false);
|
||||||
|
stream = AUDIO_TRACKS_TEST_LIST.get(ListHelper.getAudioIndexByHighestRank(
|
||||||
|
AUDIO_TRACKS_TEST_LIST, cmp));
|
||||||
|
assertEquals("en.or", stream.getId());
|
||||||
|
|
||||||
|
// Prefer descriptive audio
|
||||||
|
cmp = ListHelper.getAudioTrackComparator(Locale.ENGLISH, false, true);
|
||||||
|
stream = AUDIO_TRACKS_TEST_LIST.get(ListHelper.getAudioIndexByHighestRank(
|
||||||
|
AUDIO_TRACKS_TEST_LIST, cmp));
|
||||||
|
assertEquals("en.ds", stream.getId());
|
||||||
|
|
||||||
|
// Japanese language, fall back to original
|
||||||
|
cmp = ListHelper.getAudioTrackComparator(Locale.JAPANESE, true, false);
|
||||||
|
stream = AUDIO_TRACKS_TEST_LIST.get(ListHelper.getAudioIndexByHighestRank(
|
||||||
|
AUDIO_TRACKS_TEST_LIST, cmp));
|
||||||
|
assertEquals("en.or", stream.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -390,6 +450,22 @@ public class ListHelperTest {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static AudioStream generateAudioTrack(
|
||||||
|
@NonNull final String id,
|
||||||
|
@Nullable final String trackId,
|
||||||
|
@Nullable final Locale locale,
|
||||||
|
@Nullable final AudioTrackType trackType) {
|
||||||
|
return new AudioStream.Builder()
|
||||||
|
.setId(id)
|
||||||
|
.setContent("", true)
|
||||||
|
.setMediaFormat(MediaFormat.M4A)
|
||||||
|
.setAverageBitrate(128)
|
||||||
|
.setAudioTrackId(trackId)
|
||||||
|
.setAudioLocale(locale)
|
||||||
|
.setAudioTrackType(trackType)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
private static VideoStream generateVideoStream(@NonNull final String id,
|
private static VideoStream generateVideoStream(@NonNull final String id,
|
||||||
@Nullable final MediaFormat mediaFormat,
|
@Nullable final MediaFormat mediaFormat,
|
||||||
|
Loading…
Reference in New Issue
Block a user