mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2025-01-03 14:00:32 +00:00
feat: add track selection to downloader
This commit is contained in:
parent
fdd3b03fe5
commit
ed06f559ae
@ -191,7 +191,7 @@ dependencies {
|
|||||||
// name and the commit hash with the commit hash of the (pushed) commit you want to test
|
// name and the commit hash with the commit hash of the (pushed) commit you want to test
|
||||||
// This works thanks to JitPack: https://jitpack.io/
|
// This works thanks to JitPack: https://jitpack.io/
|
||||||
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
|
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
|
||||||
implementation 'com.github.Theta-Dev:NewPipeExtractor:3fb356a7065c75909ee3856a29be92317c295bb9'
|
implementation 'com.github.Theta-Dev:NewPipeExtractor:1aa232475e957ce5d2c036406a983db4190ebf2b'
|
||||||
implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0'
|
implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0'
|
||||||
|
|
||||||
/** Checkstyle **/
|
/** Checkstyle **/
|
||||||
|
@ -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 selectedAudioStreamIndex;
|
||||||
|
@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.selectedAudioStreamIndex =
|
||||||
|
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,38 @@ 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();
|
||||||
|
|
||||||
|
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 +306,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 +404,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 +426,29 @@ public class DownloadDialog extends DialogFragment
|
|||||||
currentInfo.getServiceId()))));
|
currentInfo.getServiceId()))));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setupAudioTrackSpinner() {
|
||||||
|
if (getContext() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dialogBinding.audioTrackSpinner.setAdapter(audioTrackAdapter);
|
||||||
|
dialogBinding.audioTrackSpinner.setSelection(selectedAudioStreamIndex);
|
||||||
|
|
||||||
|
dialogBinding.audioStreamSpinner.setAdapter(audioStreamsAdapter);
|
||||||
|
dialogBinding.audioStreamSpinner.setSelection(selectedAudioIndex);
|
||||||
|
}
|
||||||
|
|
||||||
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.setVisibility(View.VISIBLE);
|
||||||
|
dialogBinding.audioTrackSpinner.setVisibility(
|
||||||
|
wrappedAudioTracks.size() > 1 ? View.VISIBLE : View.GONE);
|
||||||
|
dialogBinding.defaultAudioTrackPresentText.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupVideoSpinner() {
|
private void setupVideoSpinner() {
|
||||||
@ -422,7 +458,21 @@ 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.defaultAudioTrackPresentText.setVisibility(
|
||||||
|
!isVideoOnly && wrappedAudioTracks.size() > 1 ? View.VISIBLE : View.GONE
|
||||||
|
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupSubtitleSpinner() {
|
private void setupSubtitleSpinner() {
|
||||||
@ -432,7 +482,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.defaultAudioTrackPresentText.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -550,18 +604,27 @@ public class DownloadDialog extends DialogFragment
|
|||||||
+ "parent = [" + parent + "], view = [" + view + "], "
|
+ "parent = [" + parent + "], view = [" + view + "], "
|
||||||
+ "position = [" + position + "], id = [" + id + "]");
|
+ "position = [" + position + "], id = [" + id + "]");
|
||||||
}
|
}
|
||||||
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
|
|
||||||
case R.id.audio_button:
|
switch (parent.getId()) {
|
||||||
|
case R.id.quality_spinner:
|
||||||
|
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
|
||||||
|
case R.id.video_button:
|
||||||
|
selectedVideoIndex = position;
|
||||||
|
onVideoStreamSelected();
|
||||||
|
break;
|
||||||
|
case R.id.subtitle_button:
|
||||||
|
selectedSubtitleIndex = position;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
onItemSelectedSetFileName();
|
||||||
|
break;
|
||||||
|
case R.id.audio_track_spinner:
|
||||||
|
selectedAudioStreamIndex = position;
|
||||||
|
updateSecondaryStreams();
|
||||||
|
break;
|
||||||
|
case R.id.audio_stream_spinner:
|
||||||
selectedAudioIndex = position;
|
selectedAudioIndex = position;
|
||||||
break;
|
|
||||||
case R.id.video_button:
|
|
||||||
selectedVideoIndex = position;
|
|
||||||
break;
|
|
||||||
case R.id.subtitle_button:
|
|
||||||
selectedSubtitleIndex = position;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
onItemSelectedSetFileName();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onItemSelectedSetFileName() {
|
private void onItemSelectedSetFileName() {
|
||||||
@ -607,6 +670,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 +721,13 @@ public class DownloadDialog extends DialogFragment
|
|||||||
dialogBinding.subtitleButton.setEnabled(enabled);
|
dialogBinding.subtitleButton.setEnabled(enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private StreamSizeWrapper<AudioStream> getWrappedAudioStreams() {
|
||||||
|
if (selectedAudioStreamIndex < 0 || selectedAudioStreamIndex > wrappedAudioTracks.size()) {
|
||||||
|
return StreamSizeWrapper.empty();
|
||||||
|
}
|
||||||
|
return wrappedAudioTracks.getTracksList().get(selectedAudioStreamIndex);
|
||||||
|
}
|
||||||
|
|
||||||
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 +1084,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);
|
||||||
|
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -111,6 +111,19 @@ public final class ListHelper {
|
|||||||
getAudioTrackComparator(context).thenComparing(getAudioFormatComparator(context)));
|
getAudioTrackComparator(context).thenComparing(getAudioFormatComparator(context)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,
|
public static int getAudioFormatIndex(final Context context,
|
||||||
final List<AudioStream> audioStreams,
|
final List<AudioStream> audioStreams,
|
||||||
@Nullable final String trackId) {
|
@Nullable final String trackId) {
|
||||||
@ -240,8 +253,50 @@ public final class ListHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sort collected streams by name
|
// Sort collected streams by name
|
||||||
return collectedStreams.values().stream().sorted(Comparator.comparing(audioStream ->
|
return collectedStreams.values().stream().sorted(getAudioTrackNameComparator(context))
|
||||||
Localization.audioTrackName(context, audioStream))).collect(Collectors.toList());
|
.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());
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
@ -413,8 +468,8 @@ public final class ListHelper {
|
|||||||
* 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 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
|
||||||
*/
|
*/
|
||||||
static int getAudioIndexByHighestRank(@Nullable final List<AudioStream> audioStreams,
|
static int getAudioIndexByHighestRank(@Nullable final List<AudioStream> audioStreams,
|
||||||
@ -615,6 +670,9 @@ public final class ListHelper {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a {@link Comparator} to compare {@link AudioStream}s by their format and bitrate.
|
* 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
|
* @param context app context
|
||||||
* @return Comparator
|
* @return Comparator
|
||||||
*/
|
*/
|
||||||
@ -628,7 +686,9 @@ public final class ListHelper {
|
|||||||
/**
|
/**
|
||||||
* Get a {@link Comparator} to compare {@link AudioStream}s by their format and bitrate.
|
* Get a {@link Comparator} to compare {@link AudioStream}s by their format and bitrate.
|
||||||
*
|
*
|
||||||
* @param defaultFormat the default format to look for
|
* <p>The prefered stream will be ordered last.</p>
|
||||||
|
*
|
||||||
|
* @param defaultFormat the default format to look for
|
||||||
* @param limitDataUsage choose low bitrate audio stream
|
* @param limitDataUsage choose low bitrate audio stream
|
||||||
* @return Comparator
|
* @return Comparator
|
||||||
*/
|
*/
|
||||||
@ -655,6 +715,21 @@ public final class ListHelper {
|
|||||||
/**
|
/**
|
||||||
* Get a {@link Comparator} to compare {@link AudioStream}s by their tracks.
|
* Get a {@link Comparator} to compare {@link AudioStream}s by their tracks.
|
||||||
*
|
*
|
||||||
|
* <p>In this order:</p>
|
||||||
|
* <ol>
|
||||||
|
* <li>If {@code preferOriginalAudio}: is 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
|
* @param context App context
|
||||||
* @return Comparator
|
* @return Comparator
|
||||||
*/
|
*/
|
||||||
@ -677,8 +752,23 @@ public final class ListHelper {
|
|||||||
/**
|
/**
|
||||||
* Get a {@link Comparator} to compare {@link AudioStream}s by their tracks.
|
* Get a {@link Comparator} to compare {@link AudioStream}s by their tracks.
|
||||||
*
|
*
|
||||||
* @param preferredLanguage Preferred audio stream language
|
* <p>In this order:</p>
|
||||||
* @param preferOriginalAudio Get the original audio track regardless of its language
|
* <ol>
|
||||||
|
* <li>If {@code preferOriginalAudio}: is 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
|
* @param preferDescriptiveAudio Prefer the descriptive audio track if available
|
||||||
* @return Comparator
|
* @return Comparator
|
||||||
*/
|
*/
|
||||||
@ -699,10 +789,26 @@ public final class ListHelper {
|
|||||||
Comparator.nullsFirst(Comparator.comparing(
|
Comparator.nullsFirst(Comparator.comparing(
|
||||||
locale -> locale.getISO3Language().equals(langCode))))
|
locale -> locale.getISO3Language().equals(langCode))))
|
||||||
.thenComparing(AudioStream::getAudioTrackType,
|
.thenComparing(AudioStream::getAudioTrackType,
|
||||||
Comparator.nullsLast(Comparator.comparingInt(trackTypeRanking::indexOf)))
|
Comparator.nullsFirst(Comparator.comparingInt(trackTypeRanking::indexOf)))
|
||||||
.thenComparing(AudioStream::getAudioLocale,
|
.thenComparing(AudioStream::getAudioLocale,
|
||||||
Comparator.nullsFirst(Comparator.comparing(
|
Comparator.nullsFirst(Comparator.comparing(
|
||||||
locale -> locale.getISO3Language().equals(
|
locale -> locale.getISO3Language().equals(
|
||||||
Locale.ENGLISH.getISO3Language()))));
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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/default_audio_track_present_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/default_audio_track_present"
|
||||||
|
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/default_audio_track_present_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"
|
||||||
|
@ -764,6 +764,7 @@
|
|||||||
<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="default_audio_track_present">The default 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>
|
||||||
|
Loading…
Reference in New Issue
Block a user