Add support of other delivery methods than progressive HTTP (in the player only)

Detailed changes:

- External players:

  - Add a message instruction about stream selection;
  - Add a message when there is no stream available for external players;
  - Return now HLS, DASH and SmoothStreaming URL contents, in addition to progressive HTTP ones.

- Player:

  - Support DASH, HLS and SmoothStreaming streams for videos, whether they are content URLs or the manifests themselves, in addition to progressive HTTP ones;
  - Use a custom HttpDataSource to play YouTube contents, based of ExoPlayer's default one, which allows better spoofing of official clients (custom user-agent and headers (depending of the client used), use of range and rn (set dynamically by the DataSource) parameters);
  - Fetch YouTube progressive contents as DASH streams, like official clients, support fully playback of livestreams which have ended recently and OTF streams;
  - Use ExoPlayer's default retries count for contents on non-fatal errors (instead of Integer.MAX_VALUE for non-live contents and 5 for live contents).

- Download dialog:

  - Add message about support of progressive HTTP streams only for downloading;
  - Remove several duplicated code and update relevant usages;
  - Support downloading of contents with an unknown media format.

- ListHelper:

  - Catch NumberFormatException when trying to compare two video streams between them.

- Tests:

  - Update ListHelperTest and StreamItemAdapterTest to fix breaking changes in the extractor.

- Other places:

  - Fixes deprecation of changes made in the extractor;
  - Improve some code related to the files changed.

- Issues fixed and/or improved with the changes:

  - Seeking of PeerTube HLS streams (the duration shown was the one from the stream duration and not the one parsed, incomplete because HLS streams are fragmented MP4s with multiple sidx boxes, for which seeking is not supported by ExoPlayer) (the app now uses the HLS manifest returned for each quality, in the master playlist (not fetched and computed by the extractor));
  - Crash when loading PeerTube streams with a separated audio;
  - Lack of some streams on some YouTube videos (OTF streams);
  - Loading times of YouTube streams, after a quality change or a playback start;
  - View count of YouTube ended livestreams interpreted as watching count (this type of streams is not interpreted anymore as livestreams);
  - Watchable time of YouTube ended livestreams;
  - Playback of SoundCloud HLS-only tracks (which cannot be downloaded anymore because the workaround which was used is being removed by SoundCloud, so it has been removed from the extractor).
This commit is contained in:
AudricV 2022-06-16 11:13:19 +02:00
parent a59660f421
commit 210834fbe9
No known key found for this signature in database
GPG Key ID: DA92EC7905614198
27 changed files with 2417 additions and 539 deletions

View File

@ -190,7 +190,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.TeamNewPipe:NewPipeExtractor:ac1c22d81c65b7b0c5427f4e1989f5256d617f32' implementation 'com.github.TeamNewPipe:NewPipeExtractor:1b51eab664ec7cbd2295c96d8b43000379cd1b7b'
/** Checkstyle **/ /** Checkstyle **/
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}" checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"

View File

@ -91,7 +91,12 @@ class StreamItemAdapterTest {
context, context,
StreamItemAdapter.StreamSizeWrapper( StreamItemAdapter.StreamSizeWrapper(
(0 until 5).map { (0 until 5).map {
SubtitlesStream(MediaFormat.SRT, "pt-BR", "https://example.com", false) SubtitlesStream.Builder()
.setContent("https://example.com", true)
.setMediaFormat(MediaFormat.SRT)
.setLanguageCode("pt-BR")
.setAutoGenerated(false)
.build()
}, },
context context
), ),
@ -108,7 +113,14 @@ class StreamItemAdapterTest {
val adapter = StreamItemAdapter<AudioStream, Stream>( val adapter = StreamItemAdapter<AudioStream, Stream>(
context, context,
StreamItemAdapter.StreamSizeWrapper( StreamItemAdapter.StreamSizeWrapper(
(0 until 5).map { AudioStream("https://example.com/$it", MediaFormat.OPUS, 192) }, (0 until 5).map {
AudioStream.Builder()
.setId(Stream.ID_UNKNOWN)
.setContent("https://example.com/$it", true)
.setMediaFormat(MediaFormat.OPUS)
.setAverageBitrate(192)
.build()
},
context context
), ),
null null
@ -126,7 +138,13 @@ class StreamItemAdapterTest {
private fun getVideoStreams(vararg videoOnly: Boolean) = private fun getVideoStreams(vararg videoOnly: Boolean) =
StreamItemAdapter.StreamSizeWrapper( StreamItemAdapter.StreamSizeWrapper(
videoOnly.map { videoOnly.map {
VideoStream("https://example.com", MediaFormat.MPEG_4, "720p", it) VideoStream.Builder()
.setId(Stream.ID_UNKNOWN)
.setContent("https://example.com", true)
.setMediaFormat(MediaFormat.MPEG_4)
.setResolution("720p")
.setIsVideoOnly(it)
.build()
}, },
context context
) )
@ -138,8 +156,16 @@ class StreamItemAdapterTest {
private fun getAudioStreams(vararg shouldBeValid: Boolean) = private fun getAudioStreams(vararg shouldBeValid: Boolean) =
getSecondaryStreamsFromList( getSecondaryStreamsFromList(
shouldBeValid.map { shouldBeValid.map {
if (it) AudioStream("https://example.com", MediaFormat.OPUS, 192) if (it) {
else null AudioStream.Builder()
.setId(Stream.ID_UNKNOWN)
.setContent("https://example.com", true)
.setMediaFormat(MediaFormat.OPUS)
.setAverageBitrate(192)
.build()
} else {
null
}
} }
) )

View File

@ -58,7 +58,6 @@ import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException;
import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException; import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo; import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
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.ktx.ExceptionUtils; import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.local.dialog.PlaylistDialog; import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.player.MainPlayer; import org.schabi.newpipe.player.MainPlayer;
@ -677,22 +676,15 @@ public class RouterActivity extends AppCompatActivity {
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> { .subscribe(result -> {
final List<VideoStream> sortedVideoStreams = ListHelper final DownloadDialog downloadDialog = DownloadDialog.newInstance(this, result);
.getSortedStreamVideosList(this, result.getVideoStreams(), downloadDialog.setSelectedVideoStream(ListHelper.getDefaultResolutionIndex(
result.getVideoOnlyStreams(), false, false); this, downloadDialog.wrappedVideoStreams.getStreamsList()));
final int selectedVideoStreamIndex = ListHelper downloadDialog.setOnDismissListener(dialog -> finish());
.getDefaultResolutionIndex(this, sortedVideoStreams);
final FragmentManager fm = getSupportFragmentManager(); final FragmentManager fm = getSupportFragmentManager();
final DownloadDialog downloadDialog = DownloadDialog.newInstance(result);
downloadDialog.setVideoStreams(sortedVideoStreams);
downloadDialog.setAudioStreams(result.getAudioStreams());
downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex);
downloadDialog.setOnDismissListener(dialog -> finish());
downloadDialog.show(fm, "downloadDialog"); downloadDialog.show(fm, "downloadDialog");
fm.executePendingTransactions(); fm.executePendingTransactions();
}, throwable -> }, throwable -> showUnsupportedUrlDialog(currentUrl)));
showUnsupportedUrlDialog(currentUrl)));
} }
@Override @Override

View File

@ -48,6 +48,7 @@ import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.localization.Localization; import org.schabi.newpipe.extractor.localization.Localization;
import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.AudioStream;
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.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.SubtitlesStream; import org.schabi.newpipe.extractor.stream.SubtitlesStream;
@ -71,6 +72,7 @@ import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Objects;
import icepick.Icepick; import icepick.Icepick;
import icepick.State; import icepick.State;
@ -82,6 +84,7 @@ import us.shandian.giga.service.DownloadManagerService;
import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder; import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder;
import us.shandian.giga.service.MissionState; import us.shandian.giga.service.MissionState;
import static org.schabi.newpipe.util.ListHelper.keepStreamsWithDelivery;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
public class DownloadDialog extends DialogFragment public class DownloadDialog extends DialogFragment
@ -92,11 +95,11 @@ public class DownloadDialog extends DialogFragment
@State @State
StreamInfo currentInfo; StreamInfo currentInfo;
@State @State
StreamSizeWrapper<AudioStream> wrappedAudioStreams = StreamSizeWrapper.empty(); public StreamSizeWrapper<AudioStream> wrappedAudioStreams = StreamSizeWrapper.empty();
@State @State
StreamSizeWrapper<VideoStream> wrappedVideoStreams = StreamSizeWrapper.empty(); public StreamSizeWrapper<VideoStream> wrappedVideoStreams = StreamSizeWrapper.empty();
@State @State
StreamSizeWrapper<SubtitlesStream> wrappedSubtitleStreams = StreamSizeWrapper.empty(); public StreamSizeWrapper<SubtitlesStream> wrappedSubtitleStreams = StreamSizeWrapper.empty();
@State @State
int selectedVideoIndex = 0; int selectedVideoIndex = 0;
@State @State
@ -138,28 +141,39 @@ public class DownloadDialog extends DialogFragment
registerForActivityResult( registerForActivityResult(
new StartActivityForResult(), this::requestDownloadPickVideoFolderResult); new StartActivityForResult(), this::requestDownloadPickVideoFolderResult);
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Instance creation // Instance creation
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
public static DownloadDialog newInstance(final StreamInfo info) { @NonNull
final DownloadDialog dialog = new DownloadDialog(); public static DownloadDialog newInstance(final Context context,
dialog.setInfo(info); @NonNull final StreamInfo info) {
return dialog; // TODO: Adapt this code when the downloader support other types of stream deliveries
} final List<VideoStream> videoStreams = new ArrayList<>(info.getVideoStreams());
final List<VideoStream> progressiveHttpVideoStreams =
keepStreamsWithDelivery(videoStreams, DeliveryMethod.PROGRESSIVE_HTTP);
public static DownloadDialog newInstance(final Context context, final StreamInfo info) { final List<VideoStream> videoOnlyStreams = new ArrayList<>(info.getVideoOnlyStreams());
final ArrayList<VideoStream> streamsList = new ArrayList<>(ListHelper final List<VideoStream> progressiveHttpVideoOnlyStreams =
.getSortedStreamVideosList(context, info.getVideoStreams(), keepStreamsWithDelivery(videoOnlyStreams, DeliveryMethod.PROGRESSIVE_HTTP);
info.getVideoOnlyStreams(), false, false));
final int selectedStreamIndex = ListHelper.getDefaultResolutionIndex(context, streamsList);
final DownloadDialog instance = newInstance(info); final List<AudioStream> audioStreams = new ArrayList<>(info.getAudioStreams());
instance.setVideoStreams(streamsList); final List<AudioStream> progressiveHttpAudioStreams =
instance.setSelectedVideoStream(selectedStreamIndex); keepStreamsWithDelivery(audioStreams, DeliveryMethod.PROGRESSIVE_HTTP);
instance.setAudioStreams(info.getAudioStreams());
instance.setSubtitleStreams(info.getSubtitles()); final List<SubtitlesStream> subtitlesStreams = new ArrayList<>(info.getSubtitles());
final List<SubtitlesStream> progressiveHttpSubtitlesStreams =
keepStreamsWithDelivery(subtitlesStreams, DeliveryMethod.PROGRESSIVE_HTTP);
final List<VideoStream> videoStreamsList = new ArrayList<>(
ListHelper.getSortedStreamVideosList(context, progressiveHttpVideoStreams,
progressiveHttpVideoOnlyStreams, false, false));
final DownloadDialog instance = new DownloadDialog();
instance.setInfo(info);
instance.setVideoStreams(videoStreamsList);
instance.setAudioStreams(progressiveHttpAudioStreams);
instance.setSubtitleStreams(progressiveHttpSubtitlesStreams);
return instance; return instance;
} }
@ -169,45 +183,69 @@ public class DownloadDialog extends DialogFragment
// Setters // Setters
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
private void setInfo(final StreamInfo info) { private void setInfo(@NonNull final StreamInfo info) {
this.currentInfo = info; this.currentInfo = info;
} }
public void setAudioStreams(final List<AudioStream> audioStreams) { public void setAudioStreams(@NonNull final List<AudioStream> audioStreams) {
setAudioStreams(new StreamSizeWrapper<>(audioStreams, getContext())); this.wrappedAudioStreams = new StreamSizeWrapper<>(audioStreams, getContext());
} }
public void setAudioStreams(final StreamSizeWrapper<AudioStream> was) { public void setVideoStreams(@NonNull final List<VideoStream> videoStreams) {
this.wrappedAudioStreams = was; this.wrappedVideoStreams = new StreamSizeWrapper<>(videoStreams, getContext());
} }
public void setVideoStreams(final List<VideoStream> videoStreams) { public void setSubtitleStreams(@NonNull final List<SubtitlesStream> subtitleStreams) {
setVideoStreams(new StreamSizeWrapper<>(videoStreams, getContext())); this.wrappedSubtitleStreams = new StreamSizeWrapper<>(subtitleStreams, getContext());
}
public void setVideoStreams(final StreamSizeWrapper<VideoStream> wvs) {
this.wrappedVideoStreams = wvs;
}
public void setSubtitleStreams(final List<SubtitlesStream> subtitleStreams) {
setSubtitleStreams(new StreamSizeWrapper<>(subtitleStreams, getContext()));
}
public void setSubtitleStreams(
final StreamSizeWrapper<SubtitlesStream> wss) {
this.wrappedSubtitleStreams = wss;
} }
/**
* Set the selected video stream, by using its index in the stream list.
*
* The index of the select video stream will be not set if this index is not in the bounds
* of the stream list.
*
* @param svi the index of the selected {@link VideoStream}
*/
public void setSelectedVideoStream(final int svi) { public void setSelectedVideoStream(final int svi) {
this.selectedVideoIndex = svi; if (selectedStreamIsInBoundsOfWrappedStreams(svi, this.wrappedVideoStreams)) {
this.selectedVideoIndex = svi;
}
} }
/**
* Set the selected audio stream, by using its index in the stream list.
*
* The index of the select audio stream will be not set if this index is not in the bounds
* of the stream list.
*
* @param sai the index of the selected {@link AudioStream}
*/
public void setSelectedAudioStream(final int sai) { public void setSelectedAudioStream(final int sai) {
this.selectedAudioIndex = sai; if (selectedStreamIsInBoundsOfWrappedStreams(sai, this.wrappedAudioStreams)) {
this.selectedAudioIndex = sai;
}
} }
/**
* Set the selected subtitles stream, by using its index in the stream list.
*
* The index of the select subtitles stream will be not set if this index is not in the bounds
* of the stream list.
*
* @param ssi the index of the selected {@link SubtitlesStream}
*/
public void setSelectedSubtitleStream(final int ssi) { public void setSelectedSubtitleStream(final int ssi) {
this.selectedSubtitleIndex = ssi; if (selectedStreamIsInBoundsOfWrappedStreams(ssi, this.wrappedSubtitleStreams)) {
this.selectedSubtitleIndex = ssi;
}
}
private boolean selectedStreamIsInBoundsOfWrappedStreams(
final int selectedIndexStream,
final StreamSizeWrapper<? extends Stream> wrappedStreams) {
return selectedIndexStream > 0
&& selectedIndexStream < wrappedStreams.getStreamsList().size();
} }
public void setOnDismissListener(@Nullable final OnDismissListener onDismissListener) { public void setOnDismissListener(@Nullable final OnDismissListener onDismissListener) {
@ -249,11 +287,16 @@ public class DownloadDialog extends DialogFragment
.getAudioStreamFor(wrappedAudioStreams.getStreamsList(), videoStreams.get(i)); .getAudioStreamFor(wrappedAudioStreams.getStreamsList(), videoStreams.get(i));
if (audioStream != null) { if (audioStream != null) {
secondaryStreams secondaryStreams.append(i, new SecondaryStreamHelper<>(wrappedAudioStreams,
.append(i, new SecondaryStreamHelper<>(wrappedAudioStreams, audioStream)); audioStream));
} else if (DEBUG) { } else if (DEBUG) {
Log.w(TAG, "No audio stream candidates for video format " final MediaFormat mediaFormat = videoStreams.get(i).getFormat();
+ videoStreams.get(i).getFormat().name()); 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");
}
} }
} }
@ -288,7 +331,8 @@ public class DownloadDialog extends DialogFragment
} }
@Override @Override
public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container, public View onCreateView(@NonNull final LayoutInflater inflater,
final ViewGroup container,
final Bundle savedInstanceState) { final Bundle savedInstanceState) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "onCreateView() called with: " Log.d(TAG, "onCreateView() called with: "
@ -299,14 +343,15 @@ public class DownloadDialog extends DialogFragment
} }
@Override @Override
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { public void onViewCreated(@NonNull final View view,
@Nullable final Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
dialogBinding = DownloadDialogBinding.bind(view); dialogBinding = DownloadDialogBinding.bind(view);
dialogBinding.fileName.setText(FilenameUtils.createFilename(getContext(), dialogBinding.fileName.setText(FilenameUtils.createFilename(getContext(),
currentInfo.getName())); currentInfo.getName()));
selectedAudioIndex = ListHelper selectedAudioIndex = ListHelper
.getDefaultAudioFormat(getContext(), currentInfo.getAudioStreams()); .getDefaultAudioFormat(getContext(), wrappedAudioStreams.getStreamsList());
selectedSubtitleIndex = getSubtitleIndexBy(subtitleStreamsAdapter.getAll()); selectedSubtitleIndex = getSubtitleIndexBy(subtitleStreamsAdapter.getAll());
@ -324,7 +369,8 @@ public class DownloadDialog extends DialogFragment
dialogBinding.threads.setProgress(threads - 1); dialogBinding.threads.setProgress(threads - 1);
dialogBinding.threads.setOnSeekBarChangeListener(new SimpleOnSeekBarChangeListener() { dialogBinding.threads.setOnSeekBarChangeListener(new SimpleOnSeekBarChangeListener() {
@Override @Override
public void onProgressChanged(@NonNull final SeekBar seekbar, final int progress, public void onProgressChanged(@NonNull final SeekBar seekbar,
final int progress,
final boolean fromUser) { final boolean fromUser) {
final int newProgress = progress + 1; final int newProgress = progress + 1;
prefs.edit().putInt(getString(R.string.default_download_threads), newProgress) prefs.edit().putInt(getString(R.string.default_download_threads), newProgress)
@ -469,7 +515,7 @@ public class DownloadDialog extends DialogFragment
result, getString(R.string.download_path_video_key), DownloadManager.TAG_VIDEO); result, getString(R.string.download_path_video_key), DownloadManager.TAG_VIDEO);
} }
private void requestDownloadSaveAsResult(final ActivityResult result) { private void requestDownloadSaveAsResult(@NonNull final ActivityResult result) {
if (result.getResultCode() != Activity.RESULT_OK) { if (result.getResultCode() != Activity.RESULT_OK) {
return; return;
} }
@ -486,8 +532,8 @@ public class DownloadDialog extends DialogFragment
return; return;
} }
final DocumentFile docFile final DocumentFile docFile = DocumentFile.fromSingleUri(context,
= DocumentFile.fromSingleUri(context, result.getData().getData()); result.getData().getData());
if (docFile == null) { if (docFile == null) {
showFailedDialog(R.string.general_error); showFailedDialog(R.string.general_error);
return; return;
@ -498,7 +544,7 @@ public class DownloadDialog extends DialogFragment
docFile.getType()); docFile.getType());
} }
private void requestDownloadPickFolderResult(final ActivityResult result, private void requestDownloadPickFolderResult(@NonNull final ActivityResult result,
final String key, final String key,
final String tag) { final String tag) {
if (result.getResultCode() != Activity.RESULT_OK) { if (result.getResultCode() != Activity.RESULT_OK) {
@ -518,12 +564,11 @@ public class DownloadDialog extends DialogFragment
StoredDirectoryHelper.PERMISSION_FLAGS); StoredDirectoryHelper.PERMISSION_FLAGS);
} }
PreferenceManager.getDefaultSharedPreferences(context).edit() PreferenceManager.getDefaultSharedPreferences(context).edit().putString(key,
.putString(key, uri.toString()).apply(); uri.toString()).apply();
try { try {
final StoredDirectoryHelper mainStorage final StoredDirectoryHelper mainStorage = new StoredDirectoryHelper(context, uri, tag);
= new StoredDirectoryHelper(context, uri, tag);
checkSelectedDownload(mainStorage, mainStorage.findFile(filenameTmp), checkSelectedDownload(mainStorage, mainStorage.findFile(filenameTmp),
filenameTmp, mimeTmp); filenameTmp, mimeTmp);
} catch (final IOException e) { } catch (final IOException e) {
@ -561,8 +606,10 @@ public class DownloadDialog extends DialogFragment
} }
@Override @Override
public void onItemSelected(final AdapterView<?> parent, final View view, public void onItemSelected(final AdapterView<?> parent,
final int position, final long id) { final View view,
final int position,
final long id) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "onItemSelected() called with: " Log.d(TAG, "onItemSelected() called with: "
+ "parent = [" + parent + "], view = [" + view + "], " + "parent = [" + parent + "], view = [" + view + "], "
@ -597,14 +644,16 @@ public class DownloadDialog extends DialogFragment
final boolean isAudioStreamsAvailable = audioStreamsAdapter.getCount() > 0; final boolean isAudioStreamsAvailable = audioStreamsAdapter.getCount() > 0;
final boolean isSubtitleStreamsAvailable = subtitleStreamsAdapter.getCount() > 0; final boolean isSubtitleStreamsAvailable = subtitleStreamsAdapter.getCount() > 0;
dialogBinding.audioButton.setVisibility(isAudioStreamsAvailable ? View.VISIBLE : View.GONE); dialogBinding.audioButton.setVisibility(isAudioStreamsAvailable ? View.VISIBLE
dialogBinding.videoButton.setVisibility(isVideoStreamsAvailable ? View.VISIBLE : View.GONE); : View.GONE);
dialogBinding.videoButton.setVisibility(isVideoStreamsAvailable ? View.VISIBLE
: View.GONE);
dialogBinding.subtitleButton.setVisibility(isSubtitleStreamsAvailable dialogBinding.subtitleButton.setVisibility(isSubtitleStreamsAvailable
? View.VISIBLE : View.GONE); ? View.VISIBLE : View.GONE);
prefs = PreferenceManager.getDefaultSharedPreferences(requireContext()); prefs = PreferenceManager.getDefaultSharedPreferences(requireContext());
final String defaultMedia = prefs.getString(getString(R.string.last_used_download_type), final String defaultMedia = prefs.getString(getString(R.string.last_used_download_type),
getString(R.string.last_download_type_video_key)); getString(R.string.last_download_type_video_key));
if (isVideoStreamsAvailable if (isVideoStreamsAvailable
&& (defaultMedia.equals(getString(R.string.last_download_type_video_key)))) { && (defaultMedia.equals(getString(R.string.last_download_type_video_key)))) {
@ -640,7 +689,7 @@ public class DownloadDialog extends DialogFragment
dialogBinding.subtitleButton.setEnabled(enabled); dialogBinding.subtitleButton.setEnabled(enabled);
} }
private int getSubtitleIndexBy(final List<SubtitlesStream> streams) { private int getSubtitleIndexBy(@NonNull final List<SubtitlesStream> streams) {
final Localization preferredLocalization = NewPipe.getPreferredLocalization(); final Localization preferredLocalization = NewPipe.getPreferredLocalization();
int candidate = 0; int candidate = 0;
@ -666,8 +715,10 @@ public class DownloadDialog extends DialogFragment
return candidate; return candidate;
} }
@NonNull
private String getNameEditText() { private String getNameEditText() {
final String str = dialogBinding.fileName.getText().toString().trim(); final String str = Objects.requireNonNull(dialogBinding.fileName.getText()).toString()
.trim();
return FilenameUtils.createFilename(context, str.isEmpty() ? currentInfo.getName() : str); return FilenameUtils.createFilename(context, str.isEmpty() ? currentInfo.getName() : str);
} }
@ -683,12 +734,8 @@ public class DownloadDialog extends DialogFragment
} }
private void launchDirectoryPicker(final ActivityResultLauncher<Intent> launcher) { private void launchDirectoryPicker(final ActivityResultLauncher<Intent> launcher) {
NoFileManagerSafeGuard.launchSafe( NoFileManagerSafeGuard.launchSafe(launcher, StoredDirectoryHelper.getPicker(context), TAG,
launcher, context);
StoredDirectoryHelper.getPicker(context),
TAG,
context
);
} }
private void prepareSelectedDownload() { private void prepareSelectedDownload() {
@ -710,30 +757,46 @@ public class DownloadDialog extends DialogFragment
mimeTmp = "audio/ogg"; mimeTmp = "audio/ogg";
filenameTmp += "opus"; filenameTmp += "opus";
} else { } else {
mimeTmp = format.mimeType; if (format != null) {
filenameTmp += format.suffix; mimeTmp = format.mimeType;
}
if (format != null) {
filenameTmp += format.suffix;
}
} }
break; break;
case R.id.video_button: case R.id.video_button:
selectedMediaType = getString(R.string.last_download_type_video_key); selectedMediaType = getString(R.string.last_download_type_video_key);
mainStorage = mainStorageVideo; mainStorage = mainStorageVideo;
format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat(); format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
mimeTmp = format.mimeType; if (format != null) {
filenameTmp += format.suffix; mimeTmp = format.mimeType;
}
if (format != null) {
filenameTmp += format.suffix;
}
break; break;
case R.id.subtitle_button: case R.id.subtitle_button:
selectedMediaType = getString(R.string.last_download_type_subtitle_key); selectedMediaType = getString(R.string.last_download_type_subtitle_key);
mainStorage = mainStorageVideo; // subtitle & video files go together mainStorage = mainStorageVideo; // subtitle & video files go together
format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat(); format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat();
mimeTmp = format.mimeType; if (format != null) {
filenameTmp += (format == MediaFormat.TTML ? MediaFormat.SRT : format).suffix; mimeTmp = format.mimeType;
}
if (format == MediaFormat.TTML) {
filenameTmp += MediaFormat.SRT.suffix;
} else {
if (format != null) {
filenameTmp += format.suffix;
}
}
break; break;
default: default:
throw new RuntimeException("No stream selected"); throw new RuntimeException("No stream selected");
} }
if (!askForSavePath if (!askForSavePath && (mainStorage == null
&& (mainStorage == null
|| mainStorage.isDirect() == NewPipeSettings.useStorageAccessFramework(context) || mainStorage.isDirect() == NewPipeSettings.useStorageAccessFramework(context)
|| mainStorage.isInvalidSafStorage())) { || mainStorage.isInvalidSafStorage())) {
// Pick new download folder if one of: // Pick new download folder if one of:
@ -767,18 +830,16 @@ public class DownloadDialog extends DialogFragment
initialPath = Uri.parse(initialSavePath.getAbsolutePath()); initialPath = Uri.parse(initialSavePath.getAbsolutePath());
} }
NoFileManagerSafeGuard.launchSafe( NoFileManagerSafeGuard.launchSafe(requestDownloadSaveAsLauncher,
requestDownloadSaveAsLauncher, StoredFileHelper.getNewPicker(context, filenameTmp, mimeTmp, initialPath), TAG,
StoredFileHelper.getNewPicker(context, filenameTmp, mimeTmp, initialPath), context);
TAG,
context
);
return; return;
} }
// check for existing file with the same name // check for existing file with the same name
checkSelectedDownload(mainStorage, mainStorage.findFile(filenameTmp), filenameTmp, mimeTmp); checkSelectedDownload(mainStorage, mainStorage.findFile(filenameTmp), filenameTmp,
mimeTmp);
// remember the last media type downloaded by the user // remember the last media type downloaded by the user
prefs.edit().putString(getString(R.string.last_used_download_type), selectedMediaType) prefs.edit().putString(getString(R.string.last_used_download_type), selectedMediaType)
@ -786,7 +847,8 @@ public class DownloadDialog extends DialogFragment
} }
private void checkSelectedDownload(final StoredDirectoryHelper mainStorage, private void checkSelectedDownload(final StoredDirectoryHelper mainStorage,
final Uri targetFile, final String filename, final Uri targetFile,
final String filename,
final String mime) { final String mime) {
StoredFileHelper storage; StoredFileHelper storage;
@ -947,7 +1009,7 @@ public class DownloadDialog extends DialogFragment
storage.truncate(); storage.truncate();
} }
} catch (final IOException e) { } catch (final IOException e) {
Log.e(TAG, "failed to truncate the file: " + storage.getUri().toString(), e); Log.e(TAG, "Failed to truncate the file: " + storage.getUri().toString(), e);
showFailedDialog(R.string.overwrite_failed); showFailedDialog(R.string.overwrite_failed);
return; return;
} }
@ -992,8 +1054,8 @@ public class DownloadDialog extends DialogFragment
} }
psArgs = null; psArgs = null;
final long videoSize = wrappedVideoStreams final long videoSize = wrappedVideoStreams.getSizeInBytes(
.getSizeInBytes((VideoStream) selectedStream); (VideoStream) selectedStream);
// set nearLength, only, if both sizes are fetched or known. This probably // set nearLength, only, if both sizes are fetched or known. This probably
// does not work on slow networks but is later updated in the downloader // does not work on slow networks but is later updated in the downloader
@ -1009,7 +1071,7 @@ public class DownloadDialog extends DialogFragment
if (selectedStream.getFormat() == MediaFormat.TTML) { if (selectedStream.getFormat() == MediaFormat.TTML) {
psName = Postprocessing.ALGORITHM_TTML_CONVERTER; psName = Postprocessing.ALGORITHM_TTML_CONVERTER;
psArgs = new String[]{ psArgs = new String[] {
selectedStream.getFormat().getSuffix(), selectedStream.getFormat().getSuffix(),
"false" // ignore empty frames "false" // ignore empty frames
}; };
@ -1020,17 +1082,22 @@ public class DownloadDialog extends DialogFragment
} }
if (secondaryStream == null) { if (secondaryStream == null) {
urls = new String[]{ urls = new String[] {
selectedStream.getUrl() selectedStream.getContent()
}; };
recoveryInfo = new MissionRecoveryInfo[]{ recoveryInfo = new MissionRecoveryInfo[] {
new MissionRecoveryInfo(selectedStream) new MissionRecoveryInfo(selectedStream)
}; };
} else { } else {
urls = new String[]{ if (secondaryStream.getDeliveryMethod() != DeliveryMethod.PROGRESSIVE_HTTP) {
selectedStream.getUrl(), secondaryStream.getUrl() throw new IllegalArgumentException("Unsupported stream delivery format"
+ secondaryStream.getDeliveryMethod());
}
urls = new String[] {
selectedStream.getContent(), secondaryStream.getContent()
}; };
recoveryInfo = new MissionRecoveryInfo[]{new MissionRecoveryInfo(selectedStream), recoveryInfo = new MissionRecoveryInfo[] {new MissionRecoveryInfo(selectedStream),
new MissionRecoveryInfo(secondaryStream)}; new MissionRecoveryInfo(secondaryStream)};
} }

View File

@ -94,6 +94,7 @@ import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PermissionHelper; import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.PicassoHelper; import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.StreamTypeUtil;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
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;
@ -121,6 +122,7 @@ import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientat
import static org.schabi.newpipe.player.helper.PlayerHelper.isClearingQueueConfirmationRequired; import static org.schabi.newpipe.player.helper.PlayerHelper.isClearingQueueConfirmationRequired;
import static org.schabi.newpipe.player.playqueue.PlayQueueItem.RECOVERY_UNSET; import static org.schabi.newpipe.player.playqueue.PlayQueueItem.RECOVERY_UNSET;
import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView; import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
import static org.schabi.newpipe.util.ListHelper.removeNonUrlAndTorrentStreams;
public final class VideoDetailFragment public final class VideoDetailFragment
extends BaseStateFragment<StreamInfo> extends BaseStateFragment<StreamInfo>
@ -186,8 +188,7 @@ public final class VideoDetailFragment
@Nullable @Nullable
private Disposable positionSubscriber = null; private Disposable positionSubscriber = null;
private List<VideoStream> sortedVideoStreams; private List<VideoStream> videoStreamsForExternalPlayers;
private int selectedVideoStreamIndex = -1;
private BottomSheetBehavior<FrameLayout> bottomSheetBehavior; private BottomSheetBehavior<FrameLayout> bottomSheetBehavior;
private BroadcastReceiver broadcastReceiver; private BroadcastReceiver broadcastReceiver;
@ -1547,11 +1548,13 @@ public final class VideoDetailFragment
binding.detailSubChannelThumbnailView.setImageDrawable(buddyDrawable); binding.detailSubChannelThumbnailView.setImageDrawable(buddyDrawable);
binding.detailUploaderThumbnailView.setImageDrawable(buddyDrawable); binding.detailUploaderThumbnailView.setImageDrawable(buddyDrawable);
final StreamType streamType = info.getStreamType();
if (info.getViewCount() >= 0) { if (info.getViewCount() >= 0) {
if (info.getStreamType().equals(StreamType.AUDIO_LIVE_STREAM)) { if (streamType.equals(StreamType.AUDIO_LIVE_STREAM)) {
binding.detailViewCountView.setText(Localization.listeningCount(activity, binding.detailViewCountView.setText(Localization.listeningCount(activity,
info.getViewCount())); info.getViewCount()));
} else if (info.getStreamType().equals(StreamType.LIVE_STREAM)) { } else if (streamType.equals(StreamType.LIVE_STREAM)) {
binding.detailViewCountView.setText(Localization binding.detailViewCountView.setText(Localization
.localizeWatchingCount(activity, info.getViewCount())); .localizeWatchingCount(activity, info.getViewCount()));
} else { } else {
@ -1612,14 +1615,13 @@ public final class VideoDetailFragment
binding.detailToggleSecondaryControlsView.setVisibility(View.VISIBLE); binding.detailToggleSecondaryControlsView.setVisibility(View.VISIBLE);
binding.detailSecondaryControlPanel.setVisibility(View.GONE); binding.detailSecondaryControlPanel.setVisibility(View.GONE);
sortedVideoStreams = ListHelper.getSortedStreamVideosList( final List<VideoStream> videoStreams = removeNonUrlAndTorrentStreams(
activity, new ArrayList<>(currentInfo.getVideoStreams()));
info.getVideoStreams(), final List<VideoStream> videoOnlyStreams = removeNonUrlAndTorrentStreams(
info.getVideoOnlyStreams(), new ArrayList<>(currentInfo.getVideoOnlyStreams()));
false, videoStreamsForExternalPlayers = ListHelper.getSortedStreamVideosList(activity,
false); videoStreams, videoOnlyStreams, false, false);
selectedVideoStreamIndex = ListHelper
.getDefaultResolutionIndex(activity, sortedVideoStreams);
updateProgressInfo(info); updateProgressInfo(info);
initThumbnailViews(info); initThumbnailViews(info);
showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView, showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView,
@ -1645,8 +1647,8 @@ public final class VideoDetailFragment
} }
} }
binding.detailControlsDownload.setVisibility(info.getStreamType() == StreamType.LIVE_STREAM binding.detailControlsDownload.setVisibility(
|| info.getStreamType() == StreamType.AUDIO_LIVE_STREAM ? View.GONE : View.VISIBLE); StreamTypeUtil.isLiveStream(streamType) ? View.GONE : View.VISIBLE);
binding.detailControlsBackground.setVisibility(info.getAudioStreams().isEmpty() binding.detailControlsBackground.setVisibility(info.getAudioStreams().isEmpty()
? View.GONE : View.VISIBLE); ? View.GONE : View.VISIBLE);
@ -1687,11 +1689,10 @@ public final class VideoDetailFragment
} }
try { try {
final DownloadDialog downloadDialog = DownloadDialog.newInstance(currentInfo); final DownloadDialog downloadDialog = DownloadDialog.newInstance(activity,
downloadDialog.setVideoStreams(sortedVideoStreams); currentInfo);
downloadDialog.setAudioStreams(currentInfo.getAudioStreams()); downloadDialog.setSelectedVideoStream(ListHelper.getDefaultResolutionIndex(activity,
downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex); downloadDialog.wrappedVideoStreams.getStreamsList()));
downloadDialog.setSubtitleStreams(currentInfo.getSubtitles());
downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog"); downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
} catch (final Exception e) { } catch (final Exception e) {
@ -1722,8 +1723,7 @@ public final class VideoDetailFragment
binding.detailPositionView.setVisibility(View.GONE); binding.detailPositionView.setVisibility(View.GONE);
// TODO: Remove this check when separation of concerns is done. // TODO: Remove this check when separation of concerns is done.
// (live streams weren't getting updated because they are mixed) // (live streams weren't getting updated because they are mixed)
if (!info.getStreamType().equals(StreamType.LIVE_STREAM) if (!StreamTypeUtil.isLiveStream(info.getStreamType())) {
&& !info.getStreamType().equals(StreamType.AUDIO_LIVE_STREAM)) {
return; return;
} }
} else { } else {
@ -2151,25 +2151,33 @@ public final class VideoDetailFragment
} }
private void showExternalPlaybackDialog() { private void showExternalPlaybackDialog() {
if (sortedVideoStreams == null) { if (currentInfo == null) {
return; return;
} }
final CharSequence[] resolutions = new CharSequence[sortedVideoStreams.size()];
for (int i = 0; i < sortedVideoStreams.size(); i++) { final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
resolutions[i] = sortedVideoStreams.get(i).getResolution(); builder.setTitle(R.string.select_quality_external_players);
} builder.setNegativeButton(android.R.string.cancel, null);
final AlertDialog.Builder builder = new AlertDialog.Builder(activity) builder.setNeutralButton(R.string.open_in_browser, (dialog, i) ->
.setNegativeButton(R.string.cancel, null) ShareUtils.openUrlInBrowser(requireActivity(), url));
.setNeutralButton(R.string.open_in_browser, (dialog, i) -> if (videoStreamsForExternalPlayers.isEmpty()) {
ShareUtils.openUrlInBrowser(requireActivity(), url) builder.setMessage(R.string.no_video_streams_available_for_external_players);
); } else {
// Maybe there are no video streams available, show just `open in browser` button final int selectedVideoStreamIndexForExternalPlayers =
if (resolutions.length > 0) { ListHelper.getDefaultResolutionIndex(activity, videoStreamsForExternalPlayers);
builder.setSingleChoiceItems(resolutions, selectedVideoStreamIndex, (dialog, i) -> { final CharSequence[] resolutions =
new CharSequence[videoStreamsForExternalPlayers.size()];
for (int i = 0; i < videoStreamsForExternalPlayers.size(); i++) {
resolutions[i] = videoStreamsForExternalPlayers.get(i).getResolution();
}
builder.setSingleChoiceItems(resolutions, selectedVideoStreamIndexForExternalPlayers,
(dialog, i) -> {
dialog.dismiss(); dialog.dismiss();
startOnExternalPlayer(activity, currentInfo, sortedVideoStreams.get(i)); startOnExternalPlayer(activity, currentInfo,
} videoStreamsForExternalPlayers.get(i));
); });
} }
builder.show(); builder.show();
} }

View File

@ -96,9 +96,10 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
case VIDEO_STREAM: case VIDEO_STREAM:
case LIVE_STREAM: case LIVE_STREAM:
case AUDIO_LIVE_STREAM: case AUDIO_LIVE_STREAM:
case POST_LIVE_STREAM:
case POST_LIVE_AUDIO_STREAM:
enableLongClick(item); enableLongClick(item);
break; break;
case FILE:
case NONE: case NONE:
default: default:
disableLongClick(); disableLongClick();
@ -114,7 +115,8 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
final StreamStateEntity state final StreamStateEntity state
= historyRecordManager.loadStreamState(infoItem).blockingGet()[0]; = historyRecordManager.loadStreamState(infoItem).blockingGet()[0];
if (state != null && item.getDuration() > 0 if (state != null && item.getDuration() > 0
&& item.getStreamType() != StreamType.LIVE_STREAM) { && item.getStreamType() != StreamType.LIVE_STREAM
&& item.getStreamType() != StreamType.AUDIO_LIVE_STREAM) {
itemProgressView.setMax((int) item.getDuration()); itemProgressView.setMax((int) item.getDuration());
if (itemProgressView.getVisibility() == View.VISIBLE) { if (itemProgressView.getVisibility() == View.VISIBLE) {
itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS

View File

@ -14,6 +14,8 @@ import org.schabi.newpipe.databinding.ListStreamItemBinding
import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_LIVE_STREAM import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_LIVE_STREAM
import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_STREAM import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_STREAM
import org.schabi.newpipe.extractor.stream.StreamType.LIVE_STREAM import org.schabi.newpipe.extractor.stream.StreamType.LIVE_STREAM
import org.schabi.newpipe.extractor.stream.StreamType.POST_LIVE_AUDIO_STREAM
import org.schabi.newpipe.extractor.stream.StreamType.POST_LIVE_STREAM
import org.schabi.newpipe.extractor.stream.StreamType.VIDEO_STREAM import org.schabi.newpipe.extractor.stream.StreamType.VIDEO_STREAM
import org.schabi.newpipe.util.Localization import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.PicassoHelper import org.schabi.newpipe.util.PicassoHelper
@ -109,7 +111,7 @@ data class StreamItem(
} }
override fun isLongClickable() = when (stream.streamType) { override fun isLongClickable() = when (stream.streamType) {
AUDIO_STREAM, VIDEO_STREAM, LIVE_STREAM, AUDIO_LIVE_STREAM -> true AUDIO_STREAM, VIDEO_STREAM, LIVE_STREAM, AUDIO_LIVE_STREAM, POST_LIVE_STREAM, POST_LIVE_AUDIO_STREAM -> true
else -> false else -> false
} }

View File

@ -1744,24 +1744,9 @@ public final class Player implements
if (exoPlayerIsNull()) { if (exoPlayerIsNull()) {
return; return;
} }
// Use duration of currentItem for non-live streams,
// because HLS streams are fragmented onUpdateProgress(Math.max((int) simpleExoPlayer.getCurrentPosition(), 0),
// and thus the whole duration is not available to the player (int) simpleExoPlayer.getDuration(), simpleExoPlayer.getBufferedPercentage());
// TODO: revert #6307 when introducing proper HLS support
final int duration;
if (currentItem != null
&& !StreamTypeUtil.isLiveStream(currentItem.getStreamType())
) {
// convert seconds to milliseconds
duration = (int) (currentItem.getDuration() * 1000);
} else {
duration = (int) simpleExoPlayer.getDuration();
}
onUpdateProgress(
Math.max((int) simpleExoPlayer.getCurrentPosition(), 0),
duration,
simpleExoPlayer.getBufferedPercentage()
);
} }
private Disposable getProgressUpdateDisposable() { private Disposable getProgressUpdateDisposable() {
@ -3399,6 +3384,7 @@ public final class Player implements
switch (info.getStreamType()) { switch (info.getStreamType()) {
case AUDIO_STREAM: case AUDIO_STREAM:
case POST_LIVE_AUDIO_STREAM:
binding.surfaceView.setVisibility(View.GONE); binding.surfaceView.setVisibility(View.GONE);
binding.endScreen.setVisibility(View.VISIBLE); binding.endScreen.setVisibility(View.VISIBLE);
binding.playbackEndTime.setVisibility(View.VISIBLE); binding.playbackEndTime.setVisibility(View.VISIBLE);
@ -3417,6 +3403,7 @@ public final class Player implements
break; break;
case VIDEO_STREAM: case VIDEO_STREAM:
case POST_LIVE_STREAM:
if (currentMetadata == null if (currentMetadata == null
|| !currentMetadata.getMaybeQuality().isPresent() || !currentMetadata.getMaybeQuality().isPresent()
|| (info.getVideoStreams().isEmpty() || (info.getVideoStreams().isEmpty()
@ -3484,10 +3471,10 @@ public final class Player implements
for (int i = 0; i < availableStreams.size(); i++) { for (int i = 0; i < availableStreams.size(); i++) {
final VideoStream videoStream = availableStreams.get(i); final VideoStream videoStream = availableStreams.get(i);
qualityPopupMenu.getMenu().add(POPUP_MENU_ID_QUALITY, i, Menu.NONE, MediaFormat qualityPopupMenu.getMenu().add(POPUP_MENU_ID_QUALITY, i, Menu.NONE, MediaFormat
.getNameById(videoStream.getFormatId()) + " " + videoStream.resolution); .getNameById(videoStream.getFormatId()) + " " + videoStream.getResolution());
} }
if (getSelectedVideoStream() != null) { if (getSelectedVideoStream() != null) {
binding.qualityTextView.setText(getSelectedVideoStream().resolution); binding.qualityTextView.setText(getSelectedVideoStream().getResolution());
} }
qualityPopupMenu.setOnMenuItemClickListener(this); qualityPopupMenu.setOnMenuItemClickListener(this);
qualityPopupMenu.setOnDismissListener(this); qualityPopupMenu.setOnDismissListener(this);
@ -3605,7 +3592,7 @@ public final class Player implements
} }
saveStreamProgressState(); //TODO added, check if good saveStreamProgressState(); //TODO added, check if good
final String newResolution = availableStreams.get(menuItemIndex).resolution; final String newResolution = availableStreams.get(menuItemIndex).getResolution();
setRecovery(); setRecovery();
setPlaybackQuality(newResolution); setPlaybackQuality(newResolution);
reloadPlayQueueManager(); reloadPlayQueueManager();
@ -3633,7 +3620,7 @@ public final class Player implements
} }
isSomePopupMenuVisible = false; //TODO check if this works isSomePopupMenuVisible = false; //TODO check if this works
if (getSelectedVideoStream() != null) { if (getSelectedVideoStream() != null) {
binding.qualityTextView.setText(getSelectedVideoStream().resolution); binding.qualityTextView.setText(getSelectedVideoStream().getResolution());
} }
if (isPlaying()) { if (isPlaying()) {
hideControls(DEFAULT_CONTROLS_DURATION, 0); hideControls(DEFAULT_CONTROLS_DURATION, 0);
@ -4250,7 +4237,8 @@ public final class Player implements
} else { } else {
final StreamType streamType = info.getStreamType(); final StreamType streamType = info.getStreamType();
if (streamType == StreamType.AUDIO_STREAM if (streamType == StreamType.AUDIO_STREAM
|| streamType == StreamType.AUDIO_LIVE_STREAM) { || streamType == StreamType.AUDIO_LIVE_STREAM
|| streamType == StreamType.POST_LIVE_AUDIO_STREAM) {
// Nothing to do more than setting the recovery position // Nothing to do more than setting the recovery position
setRecovery(); setRecovery();
return; return;
@ -4285,13 +4273,15 @@ public final class Player implements
* the content is not an audio content, but also if none of the following cases is met: * the content is not an audio content, but also if none of the following cases is met:
* *
* <ul> * <ul>
* <li>the content is an {@link StreamType#AUDIO_STREAM audio stream} or an * <li>the content is an {@link StreamType#AUDIO_STREAM audio stream}, an
* {@link StreamType#AUDIO_LIVE_STREAM audio live stream};</li> * {@link StreamType#AUDIO_LIVE_STREAM audio live stream}, or a
* {@link StreamType#POST_LIVE_AUDIO_STREAM ended audio live stream};</li>
* <li>the content is a {@link StreamType#LIVE_STREAM live stream} and the source type is a * <li>the content is a {@link StreamType#LIVE_STREAM live stream} and the source type is a
* {@link SourceType#LIVE_STREAM live source};</li> * {@link SourceType#LIVE_STREAM live source};</li>
* <li>the content's source is {@link SourceType#VIDEO_WITH_SEPARATED_AUDIO a video stream * <li>the content's source is {@link SourceType#VIDEO_WITH_SEPARATED_AUDIO a video stream
* with a separated audio source} or has no audio-only streams available <b>and</b> is a * with a separated audio source} or has no audio-only streams available <b>and</b> is a
* {@link StreamType#LIVE_STREAM live stream} or a * {@link StreamType#VIDEO_STREAM video stream}, an
* {@link StreamType#POST_LIVE_STREAM ended live stream}, or a
* {@link StreamType#LIVE_STREAM live stream}. * {@link StreamType#LIVE_STREAM live stream}.
* </li> * </li>
* </ul> * </ul>
@ -4309,14 +4299,17 @@ public final class Player implements
final StreamType streamType = streamInfo.getStreamType(); final StreamType streamType = streamInfo.getStreamType();
if (videoRendererIndex == RENDERER_UNAVAILABLE && streamType != StreamType.AUDIO_STREAM if (videoRendererIndex == RENDERER_UNAVAILABLE && streamType != StreamType.AUDIO_STREAM
&& streamType != StreamType.AUDIO_LIVE_STREAM) { && streamType != StreamType.AUDIO_LIVE_STREAM
&& streamType != StreamType.POST_LIVE_AUDIO_STREAM) {
return true; return true;
} }
// The content is an audio stream, an audio live stream, or a live stream with a live // The content is an audio stream, an audio live stream, or a live stream with a live
// source: it's not needed to reload the play queue manager because the stream source will // source: it's not needed to reload the play queue manager because the stream source will
// be the same // be the same
if ((streamType == StreamType.AUDIO_STREAM || streamType == StreamType.AUDIO_LIVE_STREAM) if ((streamType == StreamType.AUDIO_STREAM
|| streamType == StreamType.POST_LIVE_AUDIO_STREAM
|| streamType == StreamType.AUDIO_LIVE_STREAM)
|| (streamType == StreamType.LIVE_STREAM || (streamType == StreamType.LIVE_STREAM
&& sourceType == SourceType.LIVE_STREAM)) { && sourceType == SourceType.LIVE_STREAM)) {
return false; return false;
@ -4331,8 +4324,10 @@ public final class Player implements
|| (sourceType == SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY || (sourceType == SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY
&& isNullOrEmpty(streamInfo.getAudioStreams()))) { && isNullOrEmpty(streamInfo.getAudioStreams()))) {
// It's not needed to reload the play queue manager only if the content's stream type // It's not needed to reload the play queue manager only if the content's stream type
// is a video stream or a live stream // is a video stream, a live stream or an ended live stream
return streamType != StreamType.VIDEO_STREAM && streamType != StreamType.LIVE_STREAM; return streamType != StreamType.VIDEO_STREAM
&& streamType != StreamType.LIVE_STREAM
&& streamType != StreamType.POST_LIVE_STREAM;
} }
// Other cases: the play queue manager reload is needed // Other cases: the play queue manager reload is needed

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,9 @@ package org.schabi.newpipe.player.helper;
import android.content.Context; import android.content.Context;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.database.StandaloneDatabaseProvider; import com.google.android.exoplayer2.database.StandaloneDatabaseProvider;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSource; import com.google.android.exoplayer2.upstream.DefaultDataSource;
@ -14,45 +17,58 @@ import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor; import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor;
import com.google.android.exoplayer2.upstream.cache.SimpleCache; import com.google.android.exoplayer2.upstream.cache.SimpleCache;
import org.schabi.newpipe.player.datasource.YoutubeHttpDataSource;
import java.io.File; import java.io.File;
import androidx.annotation.NonNull; /* package-private */ final class CacheFactory implements DataSource.Factory {
private static final String TAG = CacheFactory.class.getSimpleName();
/* package-private */ class CacheFactory implements DataSource.Factory {
private static final String TAG = "CacheFactory";
private static final String CACHE_FOLDER_NAME = "exoplayer"; private static final String CACHE_FOLDER_NAME = "exoplayer";
private static final int CACHE_FLAGS = CacheDataSource.FLAG_BLOCK_ON_CACHE private static final int CACHE_FLAGS = CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR;
| CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR;
private final DataSource.Factory dataSourceFactory;
private final File cacheDir;
private final long maxFileSize;
// Creating cache on every instance may cause problems with multiple players when
// sources are not ExtractorMediaSource
// see: https://stackoverflow.com/questions/28700391/using-cache-in-exoplayer
// todo: make this a singleton?
private static SimpleCache cache; private static SimpleCache cache;
CacheFactory(@NonNull final Context context, private final long maxFileSize;
@NonNull final String userAgent, private final Context context;
@NonNull final TransferListener transferListener) { private final String userAgent;
this(context, userAgent, transferListener, PlayerHelper.getPreferredCacheSize(), private final TransferListener transferListener;
PlayerHelper.getPreferredFileSize()); private final DataSource.Factory upstreamDataSourceFactory;
public static class Builder {
private final Context context;
private final String userAgent;
private final TransferListener transferListener;
private DataSource.Factory upstreamDataSourceFactory;
Builder(@NonNull final Context context,
@NonNull final String userAgent,
@NonNull final TransferListener transferListener) {
this.context = context;
this.userAgent = userAgent;
this.transferListener = transferListener;
}
public void setUpstreamDataSourceFactory(
@Nullable final DataSource.Factory upstreamDataSourceFactory) {
this.upstreamDataSourceFactory = upstreamDataSourceFactory;
}
public CacheFactory build() {
return new CacheFactory(context, userAgent, transferListener,
upstreamDataSourceFactory);
}
} }
private CacheFactory(@NonNull final Context context, private CacheFactory(@NonNull final Context context,
@NonNull final String userAgent, @NonNull final String userAgent,
@NonNull final TransferListener transferListener, @NonNull final TransferListener transferListener,
final long maxCacheSize, @Nullable final DataSource.Factory upstreamDataSourceFactory) {
final long maxFileSize) { this.context = context;
this.maxFileSize = maxFileSize; this.userAgent = userAgent;
this.transferListener = transferListener;
this.upstreamDataSourceFactory = upstreamDataSourceFactory;
dataSourceFactory = new DefaultDataSource final File cacheDir = new File(context.getExternalCacheDir(), CACHE_FOLDER_NAME);
.Factory(context, new DefaultHttpDataSource.Factory().setUserAgent(userAgent))
.setTransferListener(transferListener);
cacheDir = new File(context.getExternalCacheDir(), CACHE_FOLDER_NAME);
if (!cacheDir.exists()) { if (!cacheDir.exists()) {
//noinspection ResultOfMethodCallIgnored //noinspection ResultOfMethodCallIgnored
cacheDir.mkdir(); cacheDir.mkdir();
@ -60,37 +76,43 @@ import androidx.annotation.NonNull;
if (cache == null) { if (cache == null) {
final LeastRecentlyUsedCacheEvictor evictor final LeastRecentlyUsedCacheEvictor evictor
= new LeastRecentlyUsedCacheEvictor(maxCacheSize); = new LeastRecentlyUsedCacheEvictor(PlayerHelper.getPreferredCacheSize());
cache = new SimpleCache(cacheDir, evictor, new StandaloneDatabaseProvider(context)); cache = new SimpleCache(cacheDir, evictor, new StandaloneDatabaseProvider(context));
Log.d(TAG, "initExoPlayerCache: cacheDir = " + cacheDir.getAbsolutePath());
} }
maxFileSize = PlayerHelper.getPreferredFileSize();
} }
@NonNull @NonNull
@Override @Override
public DataSource createDataSource() { public DataSource createDataSource() {
Log.d(TAG, "initExoPlayerCache: cacheDir = " + cacheDir.getAbsolutePath());
final DataSource dataSource = dataSourceFactory.createDataSource(); final DataSource.Factory upstreamDataSourceFactoryToUse;
if (upstreamDataSourceFactory == null) {
upstreamDataSourceFactoryToUse = new DefaultHttpDataSource.Factory()
.setUserAgent(userAgent);
} else {
if (upstreamDataSourceFactory instanceof DefaultHttpDataSource.Factory) {
upstreamDataSourceFactoryToUse =
((DefaultHttpDataSource.Factory) upstreamDataSourceFactory)
.setUserAgent(userAgent);
} else if (upstreamDataSourceFactory instanceof YoutubeHttpDataSource.Factory) {
upstreamDataSourceFactoryToUse =
((YoutubeHttpDataSource.Factory) upstreamDataSourceFactory)
.setUserAgentForNonMobileStreams(userAgent);
} else {
upstreamDataSourceFactoryToUse = upstreamDataSourceFactory;
}
}
final DefaultDataSource dataSource = new DefaultDataSource.Factory(context,
upstreamDataSourceFactoryToUse)
.setTransferListener(transferListener)
.createDataSource();
final FileDataSource fileSource = new FileDataSource(); final FileDataSource fileSource = new FileDataSource();
final CacheDataSink dataSink = new CacheDataSink(cache, maxFileSize); final CacheDataSink dataSink = new CacheDataSink(cache, maxFileSize);
return new CacheDataSource(cache, dataSource, fileSource, dataSink, CACHE_FLAGS, null); return new CacheDataSource(cache, dataSource, fileSource, dataSink, CACHE_FLAGS, null);
} }
public void tryDeleteCacheFiles() {
if (!cacheDir.exists() || !cacheDir.isDirectory()) {
return;
}
try {
for (final File file : cacheDir.listFiles()) {
final String filePath = file.getAbsolutePath();
final boolean deleteSuccessful = file.delete();
Log.d(TAG, "tryDeleteCacheFiles: " + filePath + " deleted = " + deleteSuccessful);
}
} catch (final Exception e) {
Log.e(TAG, "Failed to delete file.", e);
}
}
} }

View File

@ -0,0 +1,50 @@
package org.schabi.newpipe.player.helper;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist;
import com.google.android.exoplayer2.source.hls.playlist.HlsMultivariantPlaylist;
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist;
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParserFactory;
import com.google.android.exoplayer2.upstream.ParsingLoadable;
import java.io.IOException;
import java.io.InputStream;
/**
* A {@link HlsPlaylistParserFactory} for non-URI HLS sources.
*/
public final class NonUriHlsPlaylistParserFactory implements HlsPlaylistParserFactory {
private final HlsPlaylist hlsPlaylist;
public NonUriHlsPlaylistParserFactory(final HlsPlaylist hlsPlaylist) {
this.hlsPlaylist = hlsPlaylist;
}
private final class NonUriHlsPlayListParser implements ParsingLoadable.Parser<HlsPlaylist> {
@Override
public HlsPlaylist parse(final Uri uri,
final InputStream inputStream) throws IOException {
return hlsPlaylist;
}
}
@NonNull
@Override
public ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser() {
return new NonUriHlsPlayListParser();
}
@NonNull
@Override
public ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser(
@NonNull final HlsMultivariantPlaylist multivariantPlaylist,
@Nullable final HlsMediaPlaylist previousMediaPlaylist) {
return new NonUriHlsPlayListParser();
}
}

View File

@ -2,21 +2,27 @@ package org.schabi.newpipe.player.helper;
import android.content.Context; import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.source.ProgressiveMediaSource; import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.source.SingleSampleMediaSource; import com.google.android.exoplayer2.source.SingleSampleMediaSource;
import com.google.android.exoplayer2.source.dash.DashMediaSource; import com.google.android.exoplayer2.source.dash.DashMediaSource;
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
import com.google.android.exoplayer2.source.hls.HlsMediaSource; import com.google.android.exoplayer2.source.hls.HlsMediaSource;
import com.google.android.exoplayer2.source.hls.playlist.DefaultHlsPlaylistTracker; import com.google.android.exoplayer2.source.hls.playlist.DefaultHlsPlaylistTracker;
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParserFactory;
import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource; import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource;
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSource; import com.google.android.exoplayer2.upstream.DefaultDataSource;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.upstream.TransferListener;
import androidx.annotation.NonNull; import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeOtfDashManifestCreator;
import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubePostLiveStreamDvrDashManifestCreator;
import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeProgressiveDashManifestCreator;
import org.schabi.newpipe.player.datasource.YoutubeHttpDataSource;
public class PlayerDataSource { public class PlayerDataSource {
@ -29,79 +35,120 @@ public class PlayerDataSource {
* early. * early.
*/ */
private static final double PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT = 15; private static final double PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT = 15;
private static final int MANIFEST_MINIMUM_RETRY = 5;
private static final int EXTRACTOR_MINIMUM_RETRY = Integer.MAX_VALUE; /**
* The maximum number of generated manifests per cache, in
* {@link YoutubeProgressiveDashManifestCreator}, {@link YoutubeOtfDashManifestCreator} and
* {@link YoutubePostLiveStreamDvrDashManifestCreator}.
*/
private static final int MAXIMUM_SIZE_CACHED_GENERATED_MANIFESTS_PER_CACHE = 500;
private final int continueLoadingCheckIntervalBytes; private final int continueLoadingCheckIntervalBytes;
private final DataSource.Factory cacheDataSourceFactory; private final CacheFactory.Builder cacheDataSourceFactoryBuilder;
private final DataSource.Factory cachelessDataSourceFactory; private final DataSource.Factory cachelessDataSourceFactory;
public PlayerDataSource(@NonNull final Context context, public PlayerDataSource(@NonNull final Context context,
@NonNull final String userAgent, @NonNull final String userAgent,
@NonNull final TransferListener transferListener) { @NonNull final TransferListener transferListener) {
continueLoadingCheckIntervalBytes = PlayerHelper.getProgressiveLoadIntervalBytes(context); continueLoadingCheckIntervalBytes = PlayerHelper.getProgressiveLoadIntervalBytes(context);
cacheDataSourceFactory = new CacheFactory(context, userAgent, transferListener); cacheDataSourceFactoryBuilder = new CacheFactory.Builder(context, userAgent,
cachelessDataSourceFactory = new DefaultDataSource transferListener);
.Factory(context, new DefaultHttpDataSource.Factory().setUserAgent(userAgent)) cachelessDataSourceFactory = new DefaultDataSource.Factory(context,
new DefaultHttpDataSource.Factory().setUserAgent(userAgent))
.setTransferListener(transferListener); .setTransferListener(transferListener);
YoutubeProgressiveDashManifestCreator.getCache().setMaximumSize(
MAXIMUM_SIZE_CACHED_GENERATED_MANIFESTS_PER_CACHE);
YoutubeOtfDashManifestCreator.getCache().setMaximumSize(
MAXIMUM_SIZE_CACHED_GENERATED_MANIFESTS_PER_CACHE);
YoutubePostLiveStreamDvrDashManifestCreator.getCache().setMaximumSize(
MAXIMUM_SIZE_CACHED_GENERATED_MANIFESTS_PER_CACHE);
} }
public SsMediaSource.Factory getLiveSsMediaSourceFactory() { public SsMediaSource.Factory getLiveSsMediaSourceFactory() {
return new SsMediaSource.Factory( return getSSMediaSourceFactory().setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS);
new DefaultSsChunkSource.Factory(cachelessDataSourceFactory),
cachelessDataSourceFactory
)
.setLoadErrorHandlingPolicy(
new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY))
.setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS);
} }
public HlsMediaSource.Factory getLiveHlsMediaSourceFactory() { public HlsMediaSource.Factory getLiveHlsMediaSourceFactory() {
return new HlsMediaSource.Factory(cachelessDataSourceFactory) return new HlsMediaSource.Factory(cachelessDataSourceFactory)
.setAllowChunklessPreparation(true) .setAllowChunklessPreparation(true)
.setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(
MANIFEST_MINIMUM_RETRY))
.setPlaylistTrackerFactory((dataSourceFactory, loadErrorHandlingPolicy, .setPlaylistTrackerFactory((dataSourceFactory, loadErrorHandlingPolicy,
playlistParserFactory) -> playlistParserFactory) ->
new DefaultHlsPlaylistTracker(dataSourceFactory, loadErrorHandlingPolicy, new DefaultHlsPlaylistTracker(dataSourceFactory, loadErrorHandlingPolicy,
playlistParserFactory, PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT) playlistParserFactory,
); PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT));
} }
public DashMediaSource.Factory getLiveDashMediaSourceFactory() { public DashMediaSource.Factory getLiveDashMediaSourceFactory() {
return new DashMediaSource.Factory( return new DashMediaSource.Factory(
getDefaultDashChunkSourceFactory(cachelessDataSourceFactory), getDefaultDashChunkSourceFactory(cachelessDataSourceFactory),
cachelessDataSourceFactory cachelessDataSourceFactory);
)
.setLoadErrorHandlingPolicy(
new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY));
} }
private DefaultDashChunkSource.Factory getDefaultDashChunkSourceFactory( public HlsMediaSource.Factory getHlsMediaSourceFactory(
final DataSource.Factory dataSourceFactory @Nullable final HlsPlaylistParserFactory hlsPlaylistParserFactory) {
) { final HlsMediaSource.Factory factory = new HlsMediaSource.Factory(
return new DefaultDashChunkSource.Factory(dataSourceFactory); cacheDataSourceFactoryBuilder.build());
} if (hlsPlaylistParserFactory != null) {
factory.setPlaylistParserFactory(hlsPlaylistParserFactory);
public HlsMediaSource.Factory getHlsMediaSourceFactory() { }
return new HlsMediaSource.Factory(cacheDataSourceFactory); return factory;
} }
public DashMediaSource.Factory getDashMediaSourceFactory() { public DashMediaSource.Factory getDashMediaSourceFactory() {
return new DashMediaSource.Factory( return new DashMediaSource.Factory(
getDefaultDashChunkSourceFactory(cacheDataSourceFactory), getDefaultDashChunkSourceFactory(cacheDataSourceFactoryBuilder.build()),
cacheDataSourceFactory cacheDataSourceFactoryBuilder.build());
);
} }
public ProgressiveMediaSource.Factory getExtractorMediaSourceFactory() { public ProgressiveMediaSource.Factory getProgressiveMediaSourceFactory() {
return new ProgressiveMediaSource.Factory(cacheDataSourceFactory) return new ProgressiveMediaSource.Factory(cacheDataSourceFactoryBuilder.build())
.setContinueLoadingCheckIntervalBytes(continueLoadingCheckIntervalBytes) .setContinueLoadingCheckIntervalBytes(continueLoadingCheckIntervalBytes);
.setLoadErrorHandlingPolicy(
new DefaultLoadErrorHandlingPolicy(EXTRACTOR_MINIMUM_RETRY));
} }
public SingleSampleMediaSource.Factory getSampleMediaSourceFactory() { public SsMediaSource.Factory getSSMediaSourceFactory() {
return new SingleSampleMediaSource.Factory(cacheDataSourceFactory); return new SsMediaSource.Factory(
new DefaultSsChunkSource.Factory(cachelessDataSourceFactory),
cachelessDataSourceFactory);
}
public SingleSampleMediaSource.Factory getSingleSampleMediaSourceFactory() {
return new SingleSampleMediaSource.Factory(cacheDataSourceFactoryBuilder.build());
}
public DashMediaSource.Factory getYoutubeDashMediaSourceFactory() {
cacheDataSourceFactoryBuilder.setUpstreamDataSourceFactory(
getYoutubeHttpDataSourceFactory(true, true));
return new DashMediaSource.Factory(
getDefaultDashChunkSourceFactory(cacheDataSourceFactoryBuilder.build()),
cacheDataSourceFactoryBuilder.build());
}
public HlsMediaSource.Factory getYoutubeHlsMediaSourceFactory() {
cacheDataSourceFactoryBuilder.setUpstreamDataSourceFactory(
getYoutubeHttpDataSourceFactory(false, false));
return new HlsMediaSource.Factory(cacheDataSourceFactoryBuilder.build());
}
public ProgressiveMediaSource.Factory getYoutubeProgressiveMediaSourceFactory() {
cacheDataSourceFactoryBuilder.setUpstreamDataSourceFactory(
getYoutubeHttpDataSourceFactory(false, true));
return new ProgressiveMediaSource.Factory(cacheDataSourceFactoryBuilder.build())
.setContinueLoadingCheckIntervalBytes(continueLoadingCheckIntervalBytes);
}
@NonNull
private DefaultDashChunkSource.Factory getDefaultDashChunkSourceFactory(
final DataSource.Factory dataSourceFactory) {
return new DefaultDashChunkSource.Factory(dataSourceFactory);
}
@NonNull
private YoutubeHttpDataSource.Factory getYoutubeHttpDataSourceFactory(
final boolean rangeParameterEnabled,
final boolean rnParameterEnabled) {
return new YoutubeHttpDataSource.Factory()
.setRangeParameterEnabled(rangeParameterEnabled)
.setRnParameterEnabled(rnParameterEnabled);
} }
} }

View File

@ -3,6 +3,8 @@ package org.schabi.newpipe.player.helper;
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL; import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF; import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF;
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE; import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
import static org.schabi.newpipe.extractor.stream.AudioStream.UNKNOWN_BITRATE;
import static org.schabi.newpipe.extractor.stream.VideoStream.RESOLUTION_UNKNOWN;
import static org.schabi.newpipe.player.Player.IDLE_WINDOW_FLAGS; import static org.schabi.newpipe.player.Player.IDLE_WINDOW_FLAGS;
import static org.schabi.newpipe.player.Player.PLAYER_TYPE; import static org.schabi.newpipe.player.Player.PLAYER_TYPE;
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_ALWAYS; import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_ALWAYS;
@ -110,12 +112,14 @@ public final class PlayerHelper {
int MINIMIZE_ON_EXIT_MODE_POPUP = 2; int MINIMIZE_ON_EXIT_MODE_POPUP = 2;
} }
private PlayerHelper() { } private PlayerHelper() {
}
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// Exposed helpers // Exposed helpers
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
@NonNull
public static String getTimeString(final int milliSeconds) { public static String getTimeString(final int milliSeconds) {
final int seconds = (milliSeconds % 60000) / 1000; final int seconds = (milliSeconds % 60000) / 1000;
final int minutes = (milliSeconds % 3600000) / 60000; final int minutes = (milliSeconds % 3600000) / 60000;
@ -131,15 +135,18 @@ public final class PlayerHelper {
).toString(); ).toString();
} }
@NonNull
public static String formatSpeed(final double speed) { public static String formatSpeed(final double speed) {
return SPEED_FORMATTER.format(speed); return SPEED_FORMATTER.format(speed);
} }
@NonNull
public static String formatPitch(final double pitch) { public static String formatPitch(final double pitch) {
return PITCH_FORMATTER.format(pitch); return PITCH_FORMATTER.format(pitch);
} }
public static String subtitleMimeTypesOf(final MediaFormat format) { @NonNull
public static String subtitleMimeTypesOf(@NonNull final MediaFormat format) {
switch (format) { switch (format) {
case VTT: case VTT:
return MimeTypes.TEXT_VTT; return MimeTypes.TEXT_VTT;
@ -192,14 +199,48 @@ public final class PlayerHelper {
@NonNull @NonNull
public static String cacheKeyOf(@NonNull final StreamInfo info, public static String cacheKeyOf(@NonNull final StreamInfo info,
@NonNull final VideoStream video) { @NonNull final VideoStream videoStream) {
return info.getUrl() + video.getResolution() + video.getFormat().getName(); String cacheKey = info.getUrl() + " " + videoStream.getId();
final String resolution = videoStream.getResolution();
final MediaFormat mediaFormat = videoStream.getFormat();
if (resolution.equals(RESOLUTION_UNKNOWN) && mediaFormat == null) {
// The hash code is only used in the cache key in the case when the resolution and the
// media format are unknown
cacheKey += " " + videoStream.hashCode();
} else {
if (mediaFormat != null) {
cacheKey += " " + videoStream.getFormat().getName();
}
if (!resolution.equals(RESOLUTION_UNKNOWN)) {
cacheKey += " " + resolution;
}
}
return cacheKey;
} }
@NonNull @NonNull
public static String cacheKeyOf(@NonNull final StreamInfo info, public static String cacheKeyOf(@NonNull final StreamInfo info,
@NonNull final AudioStream audio) { @NonNull final AudioStream audioStream) {
return info.getUrl() + audio.getAverageBitrate() + audio.getFormat().getName(); String cacheKey = info.getUrl() + " " + audioStream.getId();
final int averageBitrate = audioStream.getAverageBitrate();
final MediaFormat mediaFormat = audioStream.getFormat();
if (averageBitrate == UNKNOWN_BITRATE && mediaFormat == null) {
// The hash code is only used in the cache key in the case when the resolution and the
// media format are unknown
cacheKey += " " + audioStream.hashCode();
} else {
if (mediaFormat != null) {
cacheKey += " " + audioStream.getFormat().getName();
}
if (averageBitrate != UNKNOWN_BITRATE) {
cacheKey += " " + averageBitrate;
}
}
return cacheKey;
} }
/** /**
@ -233,7 +274,7 @@ public final class PlayerHelper {
return null; return null;
} }
if (relatedItems.get(0) != null && relatedItems.get(0) instanceof StreamInfoItem if (relatedItems.get(0) instanceof StreamInfoItem
&& !urls.contains(relatedItems.get(0).getUrl())) { && !urls.contains(relatedItems.get(0).getUrl())) {
return getAutoQueuedSinglePlayQueue((StreamInfoItem) relatedItems.get(0)); return getAutoQueuedSinglePlayQueue((StreamInfoItem) relatedItems.get(0));
} }
@ -335,6 +376,7 @@ public final class PlayerHelper {
return 2 * 1024 * 1024L; // ExoPlayer CacheDataSink.MIN_RECOMMENDED_FRAGMENT_SIZE return 2 * 1024 * 1024L; // ExoPlayer CacheDataSink.MIN_RECOMMENDED_FRAGMENT_SIZE
} }
@NonNull
public static ExoTrackSelection.Factory getQualitySelector() { public static ExoTrackSelection.Factory getQualitySelector() {
return new AdaptiveTrackSelection.Factory( return new AdaptiveTrackSelection.Factory(
1000, 1000,
@ -389,7 +431,7 @@ public final class PlayerHelper {
/** /**
* @param context the Android context * @param context the Android context
* @return the screen brightness to use. A value less than 0 (the default) means to use the * @return the screen brightness to use. A value less than 0 (the default) means to use the
* preferred screen brightness * preferred screen brightness
*/ */
public static float getScreenBrightness(@NonNull final Context context) { public static float getScreenBrightness(@NonNull final Context context) {
final SharedPreferences sp = getPreferences(context); final SharedPreferences sp = getPreferences(context);
@ -480,7 +522,8 @@ public final class PlayerHelper {
return REPEAT_MODE_ONE; return REPEAT_MODE_ONE;
case REPEAT_MODE_ONE: case REPEAT_MODE_ONE:
return REPEAT_MODE_ALL; return REPEAT_MODE_ALL;
case REPEAT_MODE_ALL: default: case REPEAT_MODE_ALL:
default:
return REPEAT_MODE_OFF; return REPEAT_MODE_OFF;
} }
} }
@ -548,7 +591,7 @@ public final class PlayerHelper {
player.getContext().getResources().getDimension(R.dimen.popup_default_width); player.getContext().getResources().getDimension(R.dimen.popup_default_width);
final float popupWidth = popupRememberSizeAndPos final float popupWidth = popupRememberSizeAndPos
? player.getPrefs().getFloat(player.getContext().getString( ? player.getPrefs().getFloat(player.getContext().getString(
R.string.popup_saved_width_key), defaultSize) R.string.popup_saved_width_key), defaultSize)
: defaultSize; : defaultSize;
final float popupHeight = getMinimumVideoHeight(popupWidth); final float popupHeight = getMinimumVideoHeight(popupWidth);
@ -564,10 +607,10 @@ public final class PlayerHelper {
final int centerY = (int) (player.getScreenHeight() / 2f - popupHeight / 2f); final int centerY = (int) (player.getScreenHeight() / 2f - popupHeight / 2f);
popupLayoutParams.x = popupRememberSizeAndPos popupLayoutParams.x = popupRememberSizeAndPos
? player.getPrefs().getInt(player.getContext().getString( ? player.getPrefs().getInt(player.getContext().getString(
R.string.popup_saved_x_key), centerX) : centerX; R.string.popup_saved_x_key), centerX) : centerX;
popupLayoutParams.y = popupRememberSizeAndPos popupLayoutParams.y = popupRememberSizeAndPos
? player.getPrefs().getInt(player.getContext().getString( ? player.getPrefs().getInt(player.getContext().getString(
R.string.popup_saved_y_key), centerY) : centerY; R.string.popup_saved_y_key), centerY) : centerY;
return popupLayoutParams; return popupLayoutParams;
} }

View File

@ -32,7 +32,7 @@ class QualityClickListener(
val videoStream = player.selectedVideoStream val videoStream = player.selectedVideoStream
if (videoStream != null) { if (videoStream != null) {
player.binding.qualityTextView.text = player.binding.qualityTextView.text =
MediaFormat.getNameById(videoStream.formatId) + " " + videoStream.resolution MediaFormat.getNameById(videoStream.formatId) + " " + videoStream.getResolution()
} }
player.saveWasPlaying() player.saveWasPlaying()

View File

@ -1,13 +1,15 @@
package org.schabi.newpipe.player.resolver; package org.schabi.newpipe.player.resolver;
import static org.schabi.newpipe.util.ListHelper.removeTorrentStreams;
import android.content.Context; import android.content.Context;
import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
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.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.player.helper.PlayerDataSource; import org.schabi.newpipe.player.helper.PlayerDataSource;
@ -16,7 +18,13 @@ import org.schabi.newpipe.player.mediaitem.MediaItemTag;
import org.schabi.newpipe.player.mediaitem.StreamInfoTag; import org.schabi.newpipe.player.mediaitem.StreamInfoTag;
import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.ListHelper;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class AudioPlaybackResolver implements PlaybackResolver { public class AudioPlaybackResolver implements PlaybackResolver {
private static final String TAG = AudioPlaybackResolver.class.getSimpleName();
@NonNull @NonNull
private final Context context; private final Context context;
@NonNull @NonNull
@ -31,19 +39,28 @@ public class AudioPlaybackResolver implements PlaybackResolver {
@Override @Override
@Nullable @Nullable
public MediaSource resolve(@NonNull final StreamInfo info) { public MediaSource resolve(@NonNull final StreamInfo info) {
final MediaSource liveSource = maybeBuildLiveMediaSource(dataSource, info); final MediaSource liveSource = PlaybackResolver.maybeBuildLiveMediaSource(dataSource, info);
if (liveSource != null) { if (liveSource != null) {
return liveSource; return liveSource;
} }
final int index = ListHelper.getDefaultAudioFormat(context, info.getAudioStreams()); final List<AudioStream> audioStreams = new ArrayList<>(info.getAudioStreams());
removeTorrentStreams(audioStreams);
final int index = ListHelper.getDefaultAudioFormat(context, audioStreams);
if (index < 0 || index >= info.getAudioStreams().size()) { if (index < 0 || index >= info.getAudioStreams().size()) {
return null; return null;
} }
final AudioStream audio = info.getAudioStreams().get(index); final AudioStream audio = info.getAudioStreams().get(index);
final MediaItemTag tag = StreamInfoTag.of(info); final MediaItemTag tag = StreamInfoTag.of(info);
return buildMediaSource(dataSource, audio.getUrl(), PlayerHelper.cacheKeyOf(info, audio),
MediaFormat.getSuffixById(audio.getFormatId()), tag); try {
return PlaybackResolver.buildMediaSource(
dataSource, audio, info, PlayerHelper.cacheKeyOf(info, audio), tag);
} catch (final IOException e) {
Log.e(TAG, "Unable to create audio source:", e);
return null;
}
} }
} }

View File

@ -1,15 +1,38 @@
package org.schabi.newpipe.player.resolver; package org.schabi.newpipe.player.resolver;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
import static org.schabi.newpipe.player.helper.PlayerDataSource.LIVE_STREAM_EDGE_GAP_MILLIS;
import android.net.Uri; import android.net.Uri;
import android.text.TextUtils; import android.util.Log;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.source.dash.DashMediaSource;
import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser;
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist;
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser;
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest;
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser;
import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.extractor.services.youtube.ItagItem;
import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.CreationException;
import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeOtfDashManifestCreator;
import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubePostLiveStreamDvrDashManifestCreator;
import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeProgressiveDashManifestCreator;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.DeliveryMethod;
import org.schabi.newpipe.extractor.stream.Stream;
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.player.helper.NonUriHlsPlaylistParserFactory;
import org.schabi.newpipe.player.helper.PlayerDataSource; import org.schabi.newpipe.player.helper.PlayerDataSource;
import org.schabi.newpipe.player.mediaitem.MediaItemTag; import org.schabi.newpipe.player.mediaitem.MediaItemTag;
import org.schabi.newpipe.player.mediaitem.StreamInfoTag; import org.schabi.newpipe.player.mediaitem.StreamInfoTag;
@ -18,13 +41,17 @@ import org.schabi.newpipe.util.StreamTypeUtil;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import static org.schabi.newpipe.player.helper.PlayerDataSource.LIVE_STREAM_EDGE_GAP_MILLIS; import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> { public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
String TAG = PlaybackResolver.class.getSimpleName();
@Nullable @Nullable
default MediaSource maybeBuildLiveMediaSource(@NonNull final PlayerDataSource dataSource, static MediaSource maybeBuildLiveMediaSource(@NonNull final PlayerDataSource dataSource,
@NonNull final StreamInfo info) { @NonNull final StreamInfo info) {
final StreamType streamType = info.getStreamType(); final StreamType streamType = info.getStreamType();
if (!StreamTypeUtil.isLiveStream(streamType)) { if (!StreamTypeUtil.isLiveStream(streamType)) {
return null; return null;
@ -41,10 +68,10 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
} }
@NonNull @NonNull
default MediaSource buildLiveMediaSource(@NonNull final PlayerDataSource dataSource, static MediaSource buildLiveMediaSource(@NonNull final PlayerDataSource dataSource,
@NonNull final String sourceUrl, @NonNull final String sourceUrl,
@C.ContentType final int type, @C.ContentType final int type,
@NonNull final MediaItemTag metadata) { @NonNull final MediaItemTag metadata) {
final MediaSource.Factory factory; final MediaSource.Factory factory;
switch (type) { switch (type) {
case C.TYPE_SS: case C.TYPE_SS:
@ -67,46 +94,342 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
.setLiveConfiguration( .setLiveConfiguration(
new MediaItem.LiveConfiguration.Builder() new MediaItem.LiveConfiguration.Builder()
.setTargetOffsetMs(LIVE_STREAM_EDGE_GAP_MILLIS) .setTargetOffsetMs(LIVE_STREAM_EDGE_GAP_MILLIS)
.build() .build())
) .build());
.build()
);
} }
@NonNull @NonNull
default MediaSource buildMediaSource(@NonNull final PlayerDataSource dataSource, static MediaSource buildMediaSource(@NonNull final PlayerDataSource dataSource,
@NonNull final String sourceUrl, @NonNull final Stream stream,
@NonNull final String cacheKey, @NonNull final StreamInfo streamInfo,
@NonNull final String overrideExtension, @NonNull final String cacheKey,
@NonNull final MediaItemTag metadata) { @NonNull final MediaItemTag metadata)
final Uri uri = Uri.parse(sourceUrl); throws IOException {
@C.ContentType final int type = TextUtils.isEmpty(overrideExtension) if (streamInfo.getService() == ServiceList.YouTube) {
? Util.inferContentType(uri) : Util.inferContentType("." + overrideExtension); return createYoutubeMediaSource(stream, streamInfo, dataSource, cacheKey, metadata);
final MediaSource.Factory factory;
switch (type) {
case C.TYPE_SS:
factory = dataSource.getLiveSsMediaSourceFactory();
break;
case C.TYPE_DASH:
factory = dataSource.getDashMediaSourceFactory();
break;
case C.TYPE_HLS:
factory = dataSource.getHlsMediaSourceFactory();
break;
case C.TYPE_OTHER:
factory = dataSource.getExtractorMediaSourceFactory();
break;
default:
throw new IllegalStateException("Unsupported type: " + type);
} }
return factory.createMediaSource( final DeliveryMethod deliveryMethod = stream.getDeliveryMethod();
switch (deliveryMethod) {
case PROGRESSIVE_HTTP:
return buildProgressiveMediaSource(dataSource, stream, cacheKey, metadata);
case DASH:
return buildDashMediaSource(dataSource, stream, cacheKey, metadata);
case HLS:
return buildHlsMediaSource(dataSource, stream, cacheKey, metadata);
case SS:
return buildSSMediaSource(dataSource, stream, cacheKey, metadata);
// Torrent streams are not supported by ExoPlayer
default:
throw new IllegalArgumentException("Unsupported delivery type: " + deliveryMethod);
}
}
@NonNull
private static <T extends Stream> ProgressiveMediaSource buildProgressiveMediaSource(
@NonNull final PlayerDataSource dataSource,
@NonNull final T stream,
@NonNull final String cacheKey,
@NonNull final MediaItemTag metadata) throws IOException {
final String url = stream.getContent();
if (isNullOrEmpty(url)) {
throw new IOException(
"Try to generate a progressive media source from an empty string or from a "
+ "null object");
} else {
return dataSource.getProgressiveMediaSourceFactory().createMediaSource(
new MediaItem.Builder()
.setTag(metadata)
.setUri(Uri.parse(url))
.setCustomCacheKey(cacheKey)
.build());
}
}
@NonNull
private static <T extends Stream> DashMediaSource buildDashMediaSource(
@NonNull final PlayerDataSource dataSource,
@NonNull final T stream,
@NonNull final String cacheKey,
@NonNull final MediaItemTag metadata) throws IOException {
final boolean isUrlStream = stream.isUrl();
if (isUrlStream && isNullOrEmpty(stream.getContent())) {
throw new IOException("Try to generate a DASH media source from an empty string or "
+ "from a null object");
}
if (isUrlStream) {
return dataSource.getDashMediaSourceFactory().createMediaSource(
new MediaItem.Builder()
.setTag(metadata)
.setUri(Uri.parse(stream.getContent()))
.setCustomCacheKey(cacheKey)
.build());
} else {
String baseUrl = stream.getManifestUrl();
if (baseUrl == null) {
baseUrl = "";
}
final Uri uri = Uri.parse(baseUrl);
return dataSource.getDashMediaSourceFactory().createMediaSource(
createDashManifest(stream.getContent(), stream),
new MediaItem.Builder()
.setTag(metadata)
.setUri(uri)
.setCustomCacheKey(cacheKey)
.build());
}
}
@NonNull
private static <T extends Stream> DashManifest createDashManifest(
@NonNull final String manifestContent,
@NonNull final T stream) throws IOException {
try {
final ByteArrayInputStream dashManifestInput = new ByteArrayInputStream(
manifestContent.getBytes(StandardCharsets.UTF_8));
String baseUrl = stream.getManifestUrl();
if (baseUrl == null) {
baseUrl = "";
}
return new DashManifestParser().parse(Uri.parse(baseUrl), dashManifestInput);
} catch (final IOException e) {
throw new IOException("Error when parsing manual DASH manifest", e);
}
}
@NonNull
private static <T extends Stream> HlsMediaSource buildHlsMediaSource(
@NonNull final PlayerDataSource dataSource,
@NonNull final T stream,
@NonNull final String cacheKey,
@NonNull final MediaItemTag metadata) throws IOException {
final boolean isUrlStream = stream.isUrl();
if (isUrlStream && isNullOrEmpty(stream.getContent())) {
throw new IOException("Try to generate an HLS media source from an empty string or "
+ "from a null object");
}
if (isUrlStream) {
return dataSource.getHlsMediaSourceFactory(null).createMediaSource(
new MediaItem.Builder()
.setTag(metadata)
.setUri(Uri.parse(stream.getContent()))
.setCustomCacheKey(cacheKey)
.build());
} else {
String baseUrl = stream.getManifestUrl();
if (baseUrl == null) {
baseUrl = "";
}
final Uri uri = Uri.parse(baseUrl);
final HlsPlaylist hlsPlaylist;
try {
final ByteArrayInputStream hlsManifestInput = new ByteArrayInputStream(
stream.getContent().getBytes(StandardCharsets.UTF_8));
hlsPlaylist = new HlsPlaylistParser().parse(uri, hlsManifestInput);
} catch (final IOException e) {
throw new IOException("Error when parsing manual HLS manifest", e);
}
return dataSource.getHlsMediaSourceFactory(
new NonUriHlsPlaylistParserFactory(hlsPlaylist))
.createMediaSource(new MediaItem.Builder()
.setTag(metadata)
.setUri(Uri.parse(stream.getContent()))
.setCustomCacheKey(cacheKey)
.build());
}
}
@NonNull
private static <T extends Stream> SsMediaSource buildSSMediaSource(
@NonNull final PlayerDataSource dataSource,
@NonNull final T stream,
@NonNull final String cacheKey,
@NonNull final MediaItemTag metadata) throws IOException {
final boolean isUrlStream = stream.isUrl();
if (isUrlStream && isNullOrEmpty(stream.getContent())) {
throw new IOException("Try to generate an SmoothStreaming media source from an empty "
+ "string or from a null object");
}
if (isUrlStream) {
return dataSource.getSSMediaSourceFactory().createMediaSource(
new MediaItem.Builder()
.setTag(metadata)
.setUri(Uri.parse(stream.getContent()))
.setCustomCacheKey(cacheKey)
.build());
} else {
String baseUrl = stream.getManifestUrl();
if (baseUrl == null) {
baseUrl = "";
}
final Uri uri = Uri.parse(baseUrl);
final SsManifest smoothStreamingManifest;
try {
final ByteArrayInputStream smoothStreamingManifestInput = new ByteArrayInputStream(
stream.getContent().getBytes(StandardCharsets.UTF_8));
smoothStreamingManifest = new SsManifestParser().parse(uri,
smoothStreamingManifestInput);
} catch (final IOException e) {
throw new IOException("Error when parsing manual SmoothStreaming manifest", e);
}
return dataSource.getSSMediaSourceFactory().createMediaSource(
smoothStreamingManifest,
new MediaItem.Builder()
.setTag(metadata)
.setUri(uri)
.setCustomCacheKey(cacheKey)
.build());
}
}
private static <T extends Stream> MediaSource createYoutubeMediaSource(
final T stream,
final StreamInfo streamInfo,
final PlayerDataSource dataSource,
final String cacheKey,
final MediaItemTag metadata) throws IOException {
if (!(stream instanceof AudioStream || stream instanceof VideoStream)) {
throw new IOException("Try to generate a DASH manifest of a YouTube "
+ stream.getClass() + " " + stream.getContent());
}
final StreamType streamType = streamInfo.getStreamType();
if (streamType == StreamType.VIDEO_STREAM) {
return createYoutubeMediaSourceOfVideoStreamType(dataSource, stream, streamInfo,
cacheKey, metadata);
} else if (streamType == StreamType.POST_LIVE_STREAM) {
// If the content is not an URL, uses the DASH delivery method and if the stream type
// of the stream is a post live stream, it means that the content is an ended
// livestream so we need to generate the manifest corresponding to the content
// (which is the last segment of the stream)
try {
final ItagItem itagItem = Objects.requireNonNull(stream.getItagItem());
final String manifestString = YoutubePostLiveStreamDvrDashManifestCreator
.fromPostLiveStreamDvrStreamingUrl(stream.getContent(),
itagItem,
itagItem.getTargetDurationSec(),
streamInfo.getDuration());
return buildYoutubeManualDashMediaSource(dataSource,
createDashManifest(manifestString, stream), stream, cacheKey,
metadata);
} catch (final CreationException | NullPointerException e) {
Log.e(TAG, "Error when generating the DASH manifest of YouTube ended live stream",
e);
throw new IOException("Error when generating the DASH manifest of YouTube ended "
+ "live stream " + stream.getContent(), e);
}
} else {
throw new IllegalArgumentException("DASH manifest generation of YouTube livestreams is "
+ "not supported");
}
}
private static <T extends Stream> MediaSource createYoutubeMediaSourceOfVideoStreamType(
@NonNull final PlayerDataSource dataSource,
@NonNull final T stream,
@NonNull final StreamInfo streamInfo,
@NonNull final String cacheKey,
@NonNull final MediaItemTag metadata) throws IOException {
final DeliveryMethod deliveryMethod = stream.getDeliveryMethod();
switch (deliveryMethod) {
case PROGRESSIVE_HTTP:
if ((stream instanceof VideoStream && ((VideoStream) stream).isVideoOnly())
|| stream instanceof AudioStream) {
try {
final String manifestString = YoutubeProgressiveDashManifestCreator
.fromProgressiveStreamingUrl(stream.getContent(),
Objects.requireNonNull(stream.getItagItem()),
streamInfo.getDuration());
return buildYoutubeManualDashMediaSource(dataSource,
createDashManifest(manifestString, stream), stream, cacheKey,
metadata);
} catch (final CreationException | IOException | NullPointerException e) {
Log.w(TAG, "Error when generating or parsing DASH manifest of "
+ "YouTube progressive stream, falling back to a "
+ "ProgressiveMediaSource.", e);
return buildYoutubeProgressiveMediaSource(dataSource, stream, cacheKey,
metadata);
}
} else {
// Legacy progressive streams, subtitles are handled by
// VideoPlaybackResolver
return buildYoutubeProgressiveMediaSource(dataSource, stream, cacheKey,
metadata);
}
case DASH:
// If the content is not a URL, uses the DASH delivery method and if the stream
// type of the stream is a video stream, it means the content is an OTF stream
// so we need to generate the manifest corresponding to the content (which is
// the base URL of the OTF stream).
try {
final String manifestString = YoutubeOtfDashManifestCreator
.fromOtfStreamingUrl(stream.getContent(),
Objects.requireNonNull(stream.getItagItem()),
streamInfo.getDuration());
return buildYoutubeManualDashMediaSource(dataSource,
createDashManifest(manifestString, stream), stream, cacheKey,
metadata);
} catch (final CreationException | NullPointerException e) {
Log.e(TAG,
"Error when generating the DASH manifest of YouTube OTF stream", e);
throw new IOException(
"Error when generating the DASH manifest of YouTube OTF stream "
+ stream.getContent(), e);
}
case HLS:
return dataSource.getYoutubeHlsMediaSourceFactory().createMediaSource(
new MediaItem.Builder()
.setTag(metadata)
.setUri(Uri.parse(stream.getContent()))
.setCustomCacheKey(cacheKey)
.build());
default:
throw new IOException("Unsupported delivery method for YouTube contents: "
+ deliveryMethod);
}
}
@NonNull
private static <T extends Stream> DashMediaSource buildYoutubeManualDashMediaSource(
@NonNull final PlayerDataSource dataSource,
@NonNull final DashManifest dashManifest,
@NonNull final T stream,
@NonNull final String cacheKey,
@NonNull final MediaItemTag metadata) {
return dataSource.getYoutubeDashMediaSourceFactory().createMediaSource(dashManifest,
new MediaItem.Builder() new MediaItem.Builder()
.setTag(metadata) .setTag(metadata)
.setUri(uri) .setUri(Uri.parse(stream.getContent()))
.setCustomCacheKey(cacheKey) .setCustomCacheKey(cacheKey)
.build() .build());
); }
@NonNull
private static <T extends Stream> ProgressiveMediaSource buildYoutubeProgressiveMediaSource(
@NonNull final PlayerDataSource dataSource,
@NonNull final T stream,
@NonNull final String cacheKey,
@NonNull final MediaItemTag metadata) {
return dataSource.getYoutubeProgressiveMediaSourceFactory()
.createMediaSource(new MediaItem.Builder()
.setTag(metadata)
.setUri(Uri.parse(stream.getContent()))
.setCustomCacheKey(cacheKey)
.build());
} }
} }

View File

@ -2,6 +2,7 @@ package org.schabi.newpipe.player.resolver;
import android.content.Context; import android.content.Context;
import android.net.Uri; import android.net.Uri;
import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@ -22,13 +23,18 @@ import org.schabi.newpipe.player.mediaitem.MediaItemTag;
import org.schabi.newpipe.player.mediaitem.StreamInfoTag; import org.schabi.newpipe.player.mediaitem.StreamInfoTag;
import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.ListHelper;
import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; 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.removeNonUrlAndTorrentStreams;
import static org.schabi.newpipe.util.ListHelper.removeTorrentStreams;
public class VideoPlaybackResolver implements PlaybackResolver { public class VideoPlaybackResolver implements PlaybackResolver {
private static final String TAG = VideoPlaybackResolver.class.getSimpleName();
@NonNull @NonNull
private final Context context; private final Context context;
@NonNull @NonNull
@ -57,17 +63,22 @@ public class VideoPlaybackResolver implements PlaybackResolver {
@Override @Override
@Nullable @Nullable
public MediaSource resolve(@NonNull final StreamInfo info) { public MediaSource resolve(@NonNull final StreamInfo info) {
final MediaSource liveSource = maybeBuildLiveMediaSource(dataSource, info); final MediaSource liveSource = PlaybackResolver.maybeBuildLiveMediaSource(dataSource, info);
if (liveSource != null) { if (liveSource != null) {
streamSourceType = SourceType.LIVE_STREAM; streamSourceType = SourceType.LIVE_STREAM;
return liveSource; return liveSource;
} }
final List<MediaSource> mediaSources = new ArrayList<>(); final List<MediaSource> mediaSources = new ArrayList<>();
final List<VideoStream> videoStreams = new ArrayList<>(info.getVideoStreams());
final List<VideoStream> videoOnlyStreams = new ArrayList<>(info.getVideoOnlyStreams());
removeTorrentStreams(videoStreams);
removeTorrentStreams(videoOnlyStreams);
// Create video stream source // Create video stream source
final List<VideoStream> videos = ListHelper.getSortedStreamVideosList(context, final List<VideoStream> videos = ListHelper.getSortedStreamVideosList(context,
info.getVideoStreams(), info.getVideoOnlyStreams(), false, true); videoStreams, videoOnlyStreams, false, true);
final int index; final int index;
if (videos.isEmpty()) { if (videos.isEmpty()) {
index = -1; index = -1;
@ -82,24 +93,34 @@ public class VideoPlaybackResolver implements PlaybackResolver {
.orElse(null); .orElse(null);
if (video != null) { if (video != null) {
final MediaSource streamSource = buildMediaSource(dataSource, video.getUrl(), try {
PlayerHelper.cacheKeyOf(info, video), final MediaSource streamSource = PlaybackResolver.buildMediaSource(
MediaFormat.getSuffixById(video.getFormatId()), tag); dataSource, video, info, PlayerHelper.cacheKeyOf(info, video), tag);
mediaSources.add(streamSource); mediaSources.add(streamSource);
} catch (final IOException e) {
Log.e(TAG, "Unable to create video source:", e);
return null;
}
} }
// Create optional audio stream source // Create optional audio stream source
final List<AudioStream> audioStreams = info.getAudioStreams(); final List<AudioStream> audioStreams = info.getAudioStreams();
removeTorrentStreams(audioStreams);
final AudioStream audio = audioStreams.isEmpty() ? null : audioStreams.get( final AudioStream audio = audioStreams.isEmpty() ? null : audioStreams.get(
ListHelper.getDefaultAudioFormat(context, audioStreams)); 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())) {
final MediaSource audioSource = buildMediaSource(dataSource, audio.getUrl(), try {
PlayerHelper.cacheKeyOf(info, audio), final MediaSource audioSource = PlaybackResolver.buildMediaSource(
MediaFormat.getSuffixById(audio.getFormatId()), tag); dataSource, audio, info, PlayerHelper.cacheKeyOf(info, audio), tag);
mediaSources.add(audioSource); mediaSources.add(audioSource);
streamSourceType = SourceType.VIDEO_WITH_SEPARATED_AUDIO; streamSourceType = SourceType.VIDEO_WITH_SEPARATED_AUDIO;
} catch (final IOException e) {
Log.e(TAG, "Unable to create audio source:", e);
return null;
}
} else { } else {
streamSourceType = SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY; streamSourceType = SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY;
} }
@ -111,33 +132,35 @@ public class VideoPlaybackResolver implements PlaybackResolver {
// Below are auxiliary media sources // Below are auxiliary media sources
// Create subtitle sources // Create subtitle sources
if (info.getSubtitles() != null) { final List<SubtitlesStream> subtitlesStreams = info.getSubtitles();
for (final SubtitlesStream subtitle : info.getSubtitles()) { if (subtitlesStreams != null) {
final String mimeType = PlayerHelper.subtitleMimeTypesOf(subtitle.getFormat()); // Torrent and non URL subtitles are not supported by ExoPlayer
if (mimeType == null) { final List<SubtitlesStream> nonTorrentAndUrlStreams = removeNonUrlAndTorrentStreams(
continue; subtitlesStreams);
for (final SubtitlesStream subtitle : nonTorrentAndUrlStreams) {
final MediaFormat mediaFormat = subtitle.getFormat();
if (mediaFormat != null) {
@C.RoleFlags final int textRoleFlag = subtitle.isAutoGenerated()
? C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND
: C.ROLE_FLAG_CAPTION;
final MediaItem.SubtitleConfiguration textMediaItem =
new MediaItem.SubtitleConfiguration.Builder(
Uri.parse(subtitle.getContent()))
.setMimeType(mediaFormat.getMimeType())
.setRoleFlags(textRoleFlag)
.setLanguage(PlayerHelper.captionLanguageOf(context, subtitle))
.build();
final MediaSource textSource = dataSource.getSingleSampleMediaSourceFactory()
.createMediaSource(textMediaItem, TIME_UNSET);
mediaSources.add(textSource);
} }
final @C.RoleFlags int textRoleFlag = subtitle.isAutoGenerated()
? C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND
: C.ROLE_FLAG_CAPTION;
final MediaItem.SubtitleConfiguration textMediaItem =
new MediaItem.SubtitleConfiguration.Builder(Uri.parse(subtitle.getUrl()))
.setMimeType(mimeType)
.setRoleFlags(textRoleFlag)
.setLanguage(PlayerHelper.captionLanguageOf(context, subtitle))
.build();
final MediaSource textSource = dataSource
.getSampleMediaSourceFactory()
.createMediaSource(textMediaItem, TIME_UNSET);
mediaSources.add(textSource);
} }
} }
if (mediaSources.size() == 1) { if (mediaSources.size() == 1) {
return mediaSources.get(0); return mediaSources.get(0);
} else { } else {
return new MergingMediaSource(mediaSources.toArray( return new MergingMediaSource(true, mediaSources.toArray(new MediaSource[0]));
new MediaSource[0]));
} }
} }

View File

@ -13,6 +13,8 @@ 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.DeliveryMethod;
import org.schabi.newpipe.extractor.stream.Stream;
import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.extractor.stream.VideoStream;
import java.util.ArrayList; import java.util.ArrayList;
@ -21,6 +23,7 @@ import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
@ -37,10 +40,9 @@ public final class ListHelper {
// Audio format in order of efficiency. 0=most efficient, n=least efficient // Audio format in order of efficiency. 0=most efficient, n=least efficient
private static final List<MediaFormat> AUDIO_FORMAT_EFFICIENCY_RANKING = private static final List<MediaFormat> AUDIO_FORMAT_EFFICIENCY_RANKING =
Arrays.asList(MediaFormat.WEBMA, MediaFormat.M4A, MediaFormat.MP3); Arrays.asList(MediaFormat.WEBMA, MediaFormat.M4A, MediaFormat.MP3);
// Use a HashSet for better performance
private static final Set<String> HIGH_RESOLUTION_LIST private static final Set<String> HIGH_RESOLUTION_LIST = new HashSet<>(
// Uses a HashSet for better performance Arrays.asList("1440p", "2160p"));
= new HashSet<>(Arrays.asList("1440p", "2160p", "1440p60", "2160p60"));
private ListHelper() { } private ListHelper() { }
@ -110,6 +112,83 @@ public final class ListHelper {
} }
} }
/**
* Return a {@link Stream} list which uses the given delivery method from a {@link Stream}
* list.
*
* @param streamList the original stream list
* @param deliveryMethod the delivery method
* @param <S> the item type's class that extends {@link Stream}
* @return a stream list which uses the given delivery method
*/
@NonNull
public static <S extends Stream> List<S> keepStreamsWithDelivery(
@NonNull final List<S> streamList,
final DeliveryMethod deliveryMethod) {
if (streamList.isEmpty()) {
return Collections.emptyList();
}
final Iterator<S> streamListIterator = streamList.iterator();
while (streamListIterator.hasNext()) {
if (streamListIterator.next().getDeliveryMethod() != deliveryMethod) {
streamListIterator.remove();
}
}
return streamList;
}
/**
* Return a {@link Stream} list which only contains URL streams and non-torrent streams.
*
* @param streamList the original stream list
* @param <S> the item type's class that extends {@link Stream}
* @return a stream list which only contains URL streams and non-torrent streams
*/
@NonNull
public static <S extends Stream> List<S> removeNonUrlAndTorrentStreams(
@NonNull final List<S> streamList) {
if (streamList.isEmpty()) {
return Collections.emptyList();
}
final Iterator<S> streamListIterator = streamList.iterator();
while (streamListIterator.hasNext()) {
final S stream = streamListIterator.next();
if (!stream.isUrl() || stream.getDeliveryMethod() == DeliveryMethod.TORRENT) {
streamListIterator.remove();
}
}
return streamList;
}
/**
* Return a {@link Stream} list which only contains non-torrent streams.
*
* @param streamList the original stream list
* @param <S> the item type's class that extends {@link Stream}
* @return a stream list which only contains non-torrent streams
*/
@NonNull
public static <S extends Stream> List<S> removeTorrentStreams(
@NonNull final List<S> streamList) {
if (streamList.isEmpty()) {
return Collections.emptyList();
}
final Iterator<S> streamListIterator = streamList.iterator();
while (streamListIterator.hasNext()) {
final S stream = streamListIterator.next();
if (stream.getDeliveryMethod() == DeliveryMethod.TORRENT) {
streamListIterator.remove();
}
}
return streamList;
}
/** /**
* Join the two lists of video streams (video_only and normal videos), * Join the two lists of video streams (video_only and normal videos),
* and sort them according with default format chosen by the user. * and sort them according with default format chosen by the user.
@ -177,7 +256,7 @@ public final class ListHelper {
static int getDefaultResolutionIndex(final String defaultResolution, static int getDefaultResolutionIndex(final String defaultResolution,
final String bestResolutionKey, final String bestResolutionKey,
final MediaFormat defaultFormat, final MediaFormat defaultFormat,
final List<VideoStream> videoStreams) { @Nullable final List<VideoStream> videoStreams) {
if (videoStreams == null || videoStreams.isEmpty()) { if (videoStreams == null || videoStreams.isEmpty()) {
return -1; return -1;
} }
@ -233,7 +312,9 @@ public final class ListHelper {
.flatMap(List::stream) .flatMap(List::stream)
// Filter out higher resolutions (or not if high resolutions should always be shown) // Filter out higher resolutions (or not if high resolutions should always be shown)
.filter(stream -> showHigherResolutions .filter(stream -> showHigherResolutions
|| !HIGH_RESOLUTION_LIST.contains(stream.getResolution())) || !HIGH_RESOLUTION_LIST.contains(stream.getResolution()
// Replace any frame rate with nothing
.replaceAll("p\\d+$", "p")))
.collect(Collectors.toList()); .collect(Collectors.toList());
final HashMap<String, VideoStream> hashMap = new HashMap<>(); final HashMap<String, VideoStream> hashMap = new HashMap<>();
@ -366,8 +447,9 @@ public final class ListHelper {
* @param videoStreams the available video streams * @param videoStreams the available video streams
* @return the index of the preferred video stream * @return the index of the preferred video stream
*/ */
static int getVideoStreamIndex(final String targetResolution, final MediaFormat targetFormat, static int getVideoStreamIndex(@NonNull final String targetResolution,
final List<VideoStream> videoStreams) { final MediaFormat targetFormat,
@NonNull final List<VideoStream> videoStreams) {
int fullMatchIndex = -1; int fullMatchIndex = -1;
int fullMatchNoRefreshIndex = -1; int fullMatchNoRefreshIndex = -1;
int resMatchOnlyIndex = -1; int resMatchOnlyIndex = -1;
@ -428,7 +510,7 @@ public final class ListHelper {
* @param videoStreams the list of video streams to check * @param videoStreams the list of video streams to check
* @return the index of the preferred video stream * @return the index of the preferred video stream
*/ */
private static int getDefaultResolutionWithDefaultFormat(final Context context, private static int getDefaultResolutionWithDefaultFormat(@NonNull final Context context,
final String defaultResolution, final String defaultResolution,
final List<VideoStream> videoStreams) { final List<VideoStream> videoStreams) {
final MediaFormat defaultFormat = getDefaultFormat(context, final MediaFormat defaultFormat = getDefaultFormat(context,
@ -437,7 +519,7 @@ public final class ListHelper {
context.getString(R.string.best_resolution_key), defaultFormat, videoStreams); context.getString(R.string.best_resolution_key), defaultFormat, videoStreams);
} }
private static MediaFormat getDefaultFormat(final Context context, private static MediaFormat getDefaultFormat(@NonNull final Context context,
@StringRes final int defaultFormatKey, @StringRes final int defaultFormatKey,
@StringRes final int defaultFormatValueKey) { @StringRes final int defaultFormatValueKey) {
final SharedPreferences preferences final SharedPreferences preferences
@ -457,8 +539,8 @@ public final class ListHelper {
return defaultMediaFormat; return defaultMediaFormat;
} }
private static MediaFormat getMediaFormatFromKey(final Context context, private static MediaFormat getMediaFormatFromKey(@NonNull final Context context,
final String formatKey) { @NonNull final String formatKey) {
MediaFormat format = null; MediaFormat format = null;
if (formatKey.equals(context.getString(R.string.video_webm_key))) { if (formatKey.equals(context.getString(R.string.video_webm_key))) {
format = MediaFormat.WEBM; format = MediaFormat.WEBM;
@ -496,12 +578,20 @@ public final class ListHelper {
- formatRanking.indexOf(streamB.getFormat()); - formatRanking.indexOf(streamB.getFormat());
} }
private static int compareVideoStreamResolution(final String r1, final String r2) { private static int compareVideoStreamResolution(@NonNull final String r1,
final int res1 = Integer.parseInt(r1.replaceAll("0p\\d+$", "1") @NonNull final String r2) {
.replaceAll("[^\\d.]", "")); try {
final int res2 = Integer.parseInt(r2.replaceAll("0p\\d+$", "1") final int res1 = Integer.parseInt(r1.replaceAll("0p\\d+$", "1")
.replaceAll("[^\\d.]", "")); .replaceAll("[^\\d.]", ""));
return res1 - res2; final int res2 = Integer.parseInt(r2.replaceAll("0p\\d+$", "1")
.replaceAll("[^\\d.]", ""));
return res1 - res2;
} catch (final NumberFormatException e) {
// Consider the first one greater because we don't know if the two streams are
// different or not (a NumberFormatException was thrown so we don't know the resolution
// of one stream or of all streams)
return 1;
}
} }
// Compares the quality of two video streams. // Compares the quality of two video streams.
@ -536,7 +626,7 @@ public final class ListHelper {
* @param context App context * @param context App context
* @return maximum resolution allowed or null if there is no maximum * @return maximum resolution allowed or null if there is no maximum
*/ */
private static String getResolutionLimit(final Context context) { private static String getResolutionLimit(@NonNull final Context context) {
String resolutionLimit = null; String resolutionLimit = null;
if (isMeteredNetwork(context)) { if (isMeteredNetwork(context)) {
final SharedPreferences preferences final SharedPreferences preferences
@ -555,7 +645,7 @@ public final class ListHelper {
* @param context App context * @param context App context
* @return {@code true} if connected to a metered network * @return {@code true} if connected to a metered network
*/ */
public static boolean isMeteredNetwork(final Context context) { public static boolean isMeteredNetwork(@NonNull final Context context) {
final ConnectivityManager manager final ConnectivityManager manager
= ContextCompat.getSystemService(context, ConnectivityManager.class); = ContextCompat.getSystemService(context, ConnectivityManager.class);
if (manager == null || manager.getActiveNetworkInfo() == null) { if (manager == null || manager.getActiveNetworkInfo() == null) {

View File

@ -33,6 +33,7 @@ import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.AudioStream;
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.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem;
@ -60,7 +61,9 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.settings.SettingsActivity; import org.schabi.newpipe.settings.SettingsActivity;
import org.schabi.newpipe.util.external_communication.ShareUtils; import org.schabi.newpipe.util.external_communication.ShareUtils;
import java.util.ArrayList; import java.util.List;
import static org.schabi.newpipe.util.ListHelper.removeNonUrlAndTorrentStreams;
public final class NavigationHelper { public final class NavigationHelper {
public static final String MAIN_FRAGMENT_TAG = "main_fragment_tag"; public static final String MAIN_FRAGMENT_TAG = "main_fragment_tag";
@ -217,30 +220,44 @@ public final class NavigationHelper {
public static void playOnExternalAudioPlayer(@NonNull final Context context, public static void playOnExternalAudioPlayer(@NonNull final Context context,
@NonNull final StreamInfo info) { @NonNull final StreamInfo info) {
final int index = ListHelper.getDefaultAudioFormat(context, info.getAudioStreams()); final List<AudioStream> audioStreams = info.getAudioStreams();
if (audioStreams.isEmpty()) {
if (index == -1) {
Toast.makeText(context, R.string.audio_streams_empty, Toast.LENGTH_SHORT).show(); Toast.makeText(context, R.string.audio_streams_empty, Toast.LENGTH_SHORT).show();
return; return;
} }
final List<AudioStream> audioStreamsForExternalPlayers = removeNonUrlAndTorrentStreams(
audioStreams);
if (audioStreamsForExternalPlayers.isEmpty()) {
Toast.makeText(context, R.string.no_audio_streams_available_for_external_players,
Toast.LENGTH_SHORT).show();
return;
}
final int index = ListHelper.getDefaultAudioFormat(context,
audioStreamsForExternalPlayers);
final AudioStream audioStream = info.getAudioStreams().get(index); final AudioStream audioStream = audioStreamsForExternalPlayers.get(index);
playOnExternalPlayer(context, info.getName(), info.getUploaderName(), audioStream); playOnExternalPlayer(context, info.getName(), info.getUploaderName(), audioStream);
} }
public static void playOnExternalVideoPlayer(@NonNull final Context context, public static void playOnExternalVideoPlayer(final Context context,
@NonNull final StreamInfo info) { @NonNull final StreamInfo info) {
final ArrayList<VideoStream> videoStreamsList = new ArrayList<>( final List<VideoStream> videoStreams = info.getVideoStreams();
ListHelper.getSortedStreamVideosList(context, info.getVideoStreams(), null, false, if (videoStreams.isEmpty()) {
false));
final int index = ListHelper.getDefaultResolutionIndex(context, videoStreamsList);
if (index == -1) {
Toast.makeText(context, R.string.video_streams_empty, Toast.LENGTH_SHORT).show(); Toast.makeText(context, R.string.video_streams_empty, Toast.LENGTH_SHORT).show();
return; return;
} }
final List<VideoStream> videoStreamsForExternalPlayers =
ListHelper.getSortedStreamVideosList(context,
removeNonUrlAndTorrentStreams(videoStreams), null, false, false);
if (videoStreamsForExternalPlayers.isEmpty()) {
Toast.makeText(context, R.string.no_video_streams_available_for_external_players,
Toast.LENGTH_SHORT).show();
return;
}
final int index = ListHelper.getDefaultResolutionIndex(context,
videoStreamsForExternalPlayers);
final VideoStream videoStream = videoStreamsList.get(index); final VideoStream videoStream = videoStreamsForExternalPlayers.get(index);
playOnExternalPlayer(context, info.getName(), info.getUploaderName(), videoStream); playOnExternalPlayer(context, info.getName(), info.getUploaderName(), videoStream);
} }
@ -248,9 +265,49 @@ public final class NavigationHelper {
@Nullable final String name, @Nullable final String name,
@Nullable final String artist, @Nullable final String artist,
@NonNull final Stream stream) { @NonNull final Stream stream) {
final DeliveryMethod deliveryMethod = stream.getDeliveryMethod();
final String mimeType;
if (deliveryMethod == DeliveryMethod.PROGRESSIVE_HTTP) {
if (stream.getFormat() != null) {
mimeType = stream.getFormat().getMimeType();
} else {
if (stream instanceof AudioStream) {
mimeType = "audio/*";
} else if (stream instanceof VideoStream) {
mimeType = "video/*";
} else {
// This should never be reached, because subtitles are not opened in external
// players
return;
}
}
} else {
if (!stream.isUrl() || deliveryMethod == DeliveryMethod.TORRENT) {
Toast.makeText(context, R.string.selected_stream_external_player_not_supported,
Toast.LENGTH_SHORT).show();
return;
} else {
switch (deliveryMethod) {
case HLS:
mimeType = "application/x-mpegURL";
break;
case DASH:
mimeType = "application/dash+xml";
break;
case SS:
mimeType = "application/vnd.ms-sstr+xml";
break;
default:
// Progressive HTTP streams are handled above and torrents streams are not
// exposed to external players
mimeType = "";
}
}
}
final Intent intent = new Intent(); final Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW); intent.setAction(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.parse(stream.getUrl()), stream.getFormat().getMimeType()); intent.setDataAndType(Uri.parse(stream.getContent()), mimeType);
intent.putExtra(Intent.EXTRA_TITLE, name); intent.putExtra(Intent.EXTRA_TITLE, name);
intent.putExtra("title", name); intent.putExtra("title", name);
intent.putExtra("artist", artist); intent.putExtra("artist", artist);

View File

@ -1,6 +1,7 @@
package org.schabi.newpipe.util; package org.schabi.newpipe.util;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
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;
@ -14,7 +15,8 @@ public class SecondaryStreamHelper<T extends Stream> {
private final int position; private final int position;
private final StreamSizeWrapper<T> streams; private final StreamSizeWrapper<T> streams;
public SecondaryStreamHelper(final StreamSizeWrapper<T> streams, final T selectedStream) { public SecondaryStreamHelper(@NonNull final StreamSizeWrapper<T> streams,
final T selectedStream) {
this.streams = streams; this.streams = streams;
this.position = streams.getStreamsList().indexOf(selectedStream); this.position = streams.getStreamsList().indexOf(selectedStream);
if (this.position < 0) { if (this.position < 0) {
@ -29,33 +31,37 @@ public class SecondaryStreamHelper<T extends Stream> {
* @param videoStream desired video ONLY stream * @param videoStream desired video ONLY stream
* @return selected audio stream or null if a candidate was not found * @return selected audio stream or null if a candidate was not found
*/ */
@Nullable
public static AudioStream getAudioStreamFor(@NonNull final List<AudioStream> audioStreams, public static AudioStream getAudioStreamFor(@NonNull final List<AudioStream> audioStreams,
@NonNull final VideoStream videoStream) { @NonNull final VideoStream videoStream) {
switch (videoStream.getFormat()) { final MediaFormat mediaFormat = videoStream.getFormat();
case WEBM: if (mediaFormat != null) {
case MPEG_4:// ¿is mpeg-4 DASH? switch (mediaFormat) {
break; case WEBM:
default: case MPEG_4:// ¿is mpeg-4 DASH?
return null; break;
} default:
return null;
final boolean m4v = videoStream.getFormat() == MediaFormat.MPEG_4;
for (final AudioStream audio : audioStreams) {
if (audio.getFormat() == (m4v ? MediaFormat.M4A : MediaFormat.WEBMA)) {
return audio;
} }
}
if (m4v) { final boolean m4v = (mediaFormat == MediaFormat.MPEG_4);
return null;
}
// retry, but this time in reverse order for (final AudioStream audio : audioStreams) {
for (int i = audioStreams.size() - 1; i >= 0; i--) { if (audio.getFormat() == (m4v ? MediaFormat.M4A : MediaFormat.WEBMA)) {
final AudioStream audio = audioStreams.get(i); return audio;
if (audio.getFormat() == MediaFormat.WEBMA_OPUS) { }
return audio; }
if (m4v) {
return null;
}
// retry, but this time in reverse order
for (int i = audioStreams.size() - 1; i >= 0; i--) {
final AudioStream audio = audioStreams.get(i);
if (audio.getFormat() == MediaFormat.WEBMA_OPUS) {
return audio;
}
} }
} }

View File

@ -10,6 +10,8 @@ import android.widget.ImageView;
import android.widget.Spinner; import android.widget.Spinner;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.NonNull;
import org.schabi.newpipe.DownloaderImpl; import org.schabi.newpipe.DownloaderImpl;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.MediaFormat;
@ -87,7 +89,8 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
} }
@Override @Override
public View getDropDownView(final int position, final View convertView, public View getDropDownView(final int position,
final View convertView,
final ViewGroup parent) { final ViewGroup parent) {
return getCustomView(position, convertView, parent, true); return getCustomView(position, convertView, parent, true);
} }
@ -98,7 +101,10 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
convertView, parent, false); convertView, parent, false);
} }
private View getCustomView(final int position, final View view, final ViewGroup parent, @NonNull
private View getCustomView(final int position,
final View view,
final ViewGroup parent,
final boolean isDropdownItem) { final boolean isDropdownItem) {
View convertView = view; View convertView = view;
if (convertView == null) { if (convertView == null) {
@ -112,6 +118,7 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
final TextView sizeView = convertView.findViewById(R.id.stream_size); final TextView sizeView = convertView.findViewById(R.id.stream_size);
final T stream = getItem(position); final T stream = getItem(position);
final MediaFormat mediaFormat = stream.getFormat();
int woSoundIconVisibility = View.GONE; int woSoundIconVisibility = View.GONE;
String qualityString; String qualityString;
@ -135,24 +142,32 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
} }
} else if (stream instanceof AudioStream) { } else if (stream instanceof AudioStream) {
final AudioStream audioStream = ((AudioStream) stream); final AudioStream audioStream = ((AudioStream) stream);
qualityString = audioStream.getAverageBitrate() > 0 if (audioStream.getAverageBitrate() > 0) {
? audioStream.getAverageBitrate() + "kbps" qualityString = audioStream.getAverageBitrate() + "kbps";
: audioStream.getFormat().getName(); } else if (mediaFormat != null) {
qualityString = mediaFormat.getName();
} else {
qualityString = context.getString(R.string.unknown_quality);
}
} else if (stream instanceof SubtitlesStream) { } else if (stream instanceof SubtitlesStream) {
qualityString = ((SubtitlesStream) stream).getDisplayLanguageName(); qualityString = ((SubtitlesStream) stream).getDisplayLanguageName();
if (((SubtitlesStream) stream).isAutoGenerated()) { if (((SubtitlesStream) stream).isAutoGenerated()) {
qualityString += " (" + context.getString(R.string.caption_auto_generated) + ")"; qualityString += " (" + context.getString(R.string.caption_auto_generated) + ")";
} }
} else { } else {
qualityString = stream.getFormat().getSuffix(); if (mediaFormat != null) {
qualityString = mediaFormat.getSuffix();
} else {
qualityString = context.getString(R.string.unknown_quality);
}
} }
if (streamsWrapper.getSizeInBytes(position) > 0) { if (streamsWrapper.getSizeInBytes(position) > 0) {
final SecondaryStreamHelper<U> secondary = secondaryStreams == null ? null final SecondaryStreamHelper<U> secondary = secondaryStreams == null ? null
: secondaryStreams.get(position); : secondaryStreams.get(position);
if (secondary != null) { if (secondary != null) {
final long size final long size = secondary.getSizeInBytes()
= secondary.getSizeInBytes() + streamsWrapper.getSizeInBytes(position); + streamsWrapper.getSizeInBytes(position);
sizeView.setText(Utility.formatBytes(size)); sizeView.setText(Utility.formatBytes(size));
} else { } else {
sizeView.setText(streamsWrapper.getFormattedSize(position)); sizeView.setText(streamsWrapper.getFormattedSize(position));
@ -164,11 +179,15 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
if (stream instanceof SubtitlesStream) { if (stream instanceof SubtitlesStream) {
formatNameView.setText(((SubtitlesStream) stream).getLanguageTag()); formatNameView.setText(((SubtitlesStream) stream).getLanguageTag());
} else if (stream.getFormat() == MediaFormat.WEBMA_OPUS) {
// noinspection AndroidLintSetTextI18n
formatNameView.setText("opus");
} else { } else {
formatNameView.setText(stream.getFormat().getName()); if (mediaFormat == null) {
formatNameView.setText(context.getString(R.string.unknown_format));
} else if (mediaFormat == MediaFormat.WEBMA_OPUS) {
// noinspection AndroidLintSetTextI18n
formatNameView.setText("opus");
} else {
formatNameView.setText(mediaFormat.getName());
}
} }
qualityView.setText(qualityString); qualityView.setText(qualityString);
@ -233,6 +252,7 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
* @param streamsWrapper the wrapper * @param streamsWrapper the wrapper
* @return a {@link Single} that returns a boolean indicating if any elements were changed * @return a {@link Single} that returns a boolean indicating if any elements were changed
*/ */
@NonNull
public static <X extends Stream> Single<Boolean> fetchSizeForWrapper( public static <X extends Stream> Single<Boolean> fetchSizeForWrapper(
final StreamSizeWrapper<X> streamsWrapper) { final StreamSizeWrapper<X> streamsWrapper) {
final Callable<Boolean> fetchAndSet = () -> { final Callable<Boolean> fetchAndSet = () -> {
@ -243,7 +263,7 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
} }
final long contentLength = DownloaderImpl.getInstance().getContentLength( final long contentLength = DownloaderImpl.getInstance().getContentLength(
stream.getUrl()); stream.getContent());
streamsWrapper.setSize(stream, contentLength); streamsWrapper.setSize(stream, contentLength);
hasChanged = true; hasChanged = true;
} }

View File

@ -3,7 +3,7 @@ package org.schabi.newpipe.util;
import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.extractor.stream.StreamType;
/** /**
* Utility class for {@link org.schabi.newpipe.extractor.stream.StreamType}. * Utility class for {@link StreamType}.
*/ */
public final class StreamTypeUtil { public final class StreamTypeUtil {
private StreamTypeUtil() { private StreamTypeUtil() {
@ -11,10 +11,10 @@ public final class StreamTypeUtil {
} }
/** /**
* Checks if the streamType is a livestream. * Check if the {@link StreamType} of a stream is a livestream.
* *
* @param streamType * @param streamType the stream type of the stream
* @return <code>true</code> when the streamType is a * @return <code>true</code> if the streamType is a
* {@link StreamType#LIVE_STREAM} or {@link StreamType#AUDIO_LIVE_STREAM} * {@link StreamType#LIVE_STREAM} or {@link StreamType#AUDIO_LIVE_STREAM}
*/ */
public static boolean isLiveStream(final StreamType streamType) { public static boolean isLiveStream(final StreamType streamType) {

View File

@ -6,6 +6,7 @@ import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.DeliveryMethod;
import org.schabi.newpipe.extractor.stream.StreamExtractor; import org.schabi.newpipe.extractor.stream.StreamExtractor;
import org.schabi.newpipe.extractor.stream.SubtitlesStream; import org.schabi.newpipe.extractor.stream.SubtitlesStream;
import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.extractor.stream.VideoStream;
@ -131,31 +132,38 @@ public class DownloadMissionRecover extends Thread {
switch (mRecovery.getKind()) { switch (mRecovery.getKind()) {
case 'a': case 'a':
for (AudioStream audio : mExtractor.getAudioStreams()) { for (final AudioStream audio : mExtractor.getAudioStreams()) {
if (audio.getAverageBitrate() == mRecovery.getDesiredBitrate() && audio.getFormat() == mRecovery.getFormat()) { if (audio.getAverageBitrate() == mRecovery.getDesiredBitrate()
url = audio.getUrl(); && audio.getFormat() == mRecovery.getFormat()
&& audio.getDeliveryMethod() == DeliveryMethod.PROGRESSIVE_HTTP) {
url = audio.getContent();
break; break;
} }
} }
break; break;
case 'v': case 'v':
List<VideoStream> videoStreams; final List<VideoStream> videoStreams;
if (mRecovery.isDesired2()) if (mRecovery.isDesired2())
videoStreams = mExtractor.getVideoOnlyStreams(); videoStreams = mExtractor.getVideoOnlyStreams();
else else
videoStreams = mExtractor.getVideoStreams(); videoStreams = mExtractor.getVideoStreams();
for (VideoStream video : videoStreams) { for (final VideoStream video : videoStreams) {
if (video.resolution.equals(mRecovery.getDesired()) && video.getFormat() == mRecovery.getFormat()) { if (video.getResolution().equals(mRecovery.getDesired())
url = video.getUrl(); && video.getFormat() == mRecovery.getFormat()
&& video.getDeliveryMethod() == DeliveryMethod.PROGRESSIVE_HTTP) {
url = video.getContent();
break; break;
} }
} }
break; break;
case 's': case 's':
for (SubtitlesStream subtitles : mExtractor.getSubtitles(mRecovery.getFormat())) { for (final SubtitlesStream subtitles : mExtractor.getSubtitles(mRecovery
.getFormat())) {
String tag = subtitles.getLanguageTag(); String tag = subtitles.getLanguageTag();
if (tag.equals(mRecovery.getDesired()) && subtitles.isAutoGenerated() == mRecovery.isDesired2()) { if (tag.equals(mRecovery.getDesired())
url = subtitles.getUrl(); && subtitles.isAutoGenerated() == mRecovery.isDesired2()
&& subtitles.getDeliveryMethod() == DeliveryMethod.PROGRESSIVE_HTTP) {
url = subtitles.getContent();
break; break;
} }
} }

View File

@ -11,23 +11,23 @@ import java.io.Serializable
@Parcelize @Parcelize
class MissionRecoveryInfo( class MissionRecoveryInfo(
var format: MediaFormat, var format: MediaFormat?,
var desired: String? = null, var desired: String? = null,
var isDesired2: Boolean = false, var isDesired2: Boolean = false,
var desiredBitrate: Int = 0, var desiredBitrate: Int = 0,
var kind: Char = Char.MIN_VALUE, var kind: Char = Char.MIN_VALUE,
var validateCondition: String? = null var validateCondition: String? = null
) : Serializable, Parcelable { ) : Serializable, Parcelable {
constructor(stream: Stream) : this(format = stream.getFormat()!!) { constructor(stream: Stream) : this(format = stream.format) {
when (stream) { when (stream) {
is AudioStream -> { is AudioStream -> {
desiredBitrate = stream.averageBitrate desiredBitrate = stream.getAverageBitrate()
isDesired2 = false isDesired2 = false
kind = 'a' kind = 'a'
} }
is VideoStream -> { is VideoStream -> {
desired = stream.resolution desired = stream.getResolution()
isDesired2 = stream.isVideoOnly isDesired2 = stream.isVideoOnly()
kind = 'v' kind = 'v'
} }
is SubtitlesStream -> { is SubtitlesStream -> {
@ -62,7 +62,7 @@ class MissionRecoveryInfo(
} }
} }
str.append(" format=") str.append(" format=")
.append(format.getName()) .append(format?.getName())
.append(' ') .append(' ')
.append(info) .append(info)
.append('}') .append('}')

View File

@ -82,6 +82,7 @@
android:text="@string/msg_threads" /> android:text="@string/msg_threads" />
<LinearLayout <LinearLayout
android:id="@+id/threads_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@+id/threads_text_view" android:layout_below="@+id/threads_text_view"
@ -106,4 +107,16 @@
android:max="31" android:max="31"
android:progress="3" /> android:progress="3" />
</LinearLayout> </LinearLayout>
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/streams_hidden"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/threads_layout"
android:layout_marginLeft="24dp"
android:layout_marginRight="24dp"
android:layout_marginBottom="6dp"
android:textAlignment="textEnd"
android:text="@string/streams_not_yet_supported_removed" />
</RelativeLayout> </RelativeLayout>

View File

@ -740,4 +740,11 @@
<string name="you_successfully_subscribed">You now subscribed to this channel</string> <string name="you_successfully_subscribed">You now subscribed to this channel</string>
<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">Note that streams which are not supported by the downloader yet have been removed</string>
<string name="selected_stream_external_player_not_supported">The selected stream is not supported by external players</string>
<string name="no_audio_streams_available_for_external_players">No audio streams are available for external players</string>
<string name="no_video_streams_available_for_external_players">No video streams are available for external players</string>
<string name="select_quality_external_players">Select quality for external players</string>
<string name="unknown_format">Unknown format</string>
<string name="unknown_quality">Unknown quality</string>
</resources> </resources>

View File

@ -13,38 +13,41 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class ListHelperTest { public class ListHelperTest {
private static final String BEST_RESOLUTION_KEY = "best_resolution"; private static final String BEST_RESOLUTION_KEY = "best_resolution";
private static final List<AudioStream> AUDIO_STREAMS_TEST_LIST = Arrays.asList( private static final List<AudioStream> AUDIO_STREAMS_TEST_LIST = Arrays.asList(
new AudioStream("", MediaFormat.M4A, /**/ 128), generateAudioStream("m4a-128-1", MediaFormat.M4A, 128),
new AudioStream("", MediaFormat.WEBMA, /**/ 192), generateAudioStream("webma-192", MediaFormat.WEBMA, 192),
new AudioStream("", MediaFormat.MP3, /**/ 64), generateAudioStream("mp3-64", MediaFormat.MP3, 64),
new AudioStream("", MediaFormat.WEBMA, /**/ 192), generateAudioStream("webma-192", MediaFormat.WEBMA, 192),
new AudioStream("", MediaFormat.M4A, /**/ 128), generateAudioStream("m4a-128-2", MediaFormat.M4A, 128),
new AudioStream("", MediaFormat.MP3, /**/ 128), generateAudioStream("mp3-128", MediaFormat.MP3, 128),
new AudioStream("", MediaFormat.WEBMA, /**/ 64), generateAudioStream("webma-64", MediaFormat.WEBMA, 64),
new AudioStream("", MediaFormat.M4A, /**/ 320), generateAudioStream("m4a-320", MediaFormat.M4A, 320),
new AudioStream("", MediaFormat.MP3, /**/ 192), generateAudioStream("mp3-192", MediaFormat.MP3, 192),
new AudioStream("", MediaFormat.WEBMA, /**/ 320)); generateAudioStream("webma-320", MediaFormat.WEBMA, 320));
private static final List<VideoStream> VIDEO_STREAMS_TEST_LIST = Arrays.asList( private static final List<VideoStream> VIDEO_STREAMS_TEST_LIST = Arrays.asList(
new VideoStream("", MediaFormat.MPEG_4, /**/ "720p"), generateVideoStream("mpeg_4-720", MediaFormat.MPEG_4, "720p", false),
new VideoStream("", MediaFormat.v3GPP, /**/ "240p"), generateVideoStream("v3gpp-240", MediaFormat.v3GPP, "240p", false),
new VideoStream("", MediaFormat.WEBM, /**/ "480p"), generateVideoStream("webm-480", MediaFormat.WEBM, "480p", false),
new VideoStream("", MediaFormat.v3GPP, /**/ "144p"), generateVideoStream("v3gpp-144", MediaFormat.v3GPP, "144p", false),
new VideoStream("", MediaFormat.MPEG_4, /**/ "360p"), generateVideoStream("mpeg_4-360", MediaFormat.MPEG_4, "360p", false),
new VideoStream("", MediaFormat.WEBM, /**/ "360p")); generateVideoStream("webm-360", MediaFormat.WEBM, "360p", false));
private static final List<VideoStream> VIDEO_ONLY_STREAMS_TEST_LIST = Arrays.asList( private static final List<VideoStream> VIDEO_ONLY_STREAMS_TEST_LIST = Arrays.asList(
new VideoStream("", MediaFormat.MPEG_4, /**/ "720p", true), generateVideoStream("mpeg_4-720-1", MediaFormat.MPEG_4, "720p", true),
new VideoStream("", MediaFormat.MPEG_4, /**/ "720p", true), generateVideoStream("mpeg_4-720-2", MediaFormat.MPEG_4, "720p", true),
new VideoStream("", MediaFormat.MPEG_4, /**/ "2160p", true), generateVideoStream("mpeg_4-2160", MediaFormat.MPEG_4, "2160p", true),
new VideoStream("", MediaFormat.MPEG_4, /**/ "1440p60", true), generateVideoStream("mpeg_4-1440_60", MediaFormat.MPEG_4, "1440p60", true),
new VideoStream("", MediaFormat.WEBM, /**/ "720p60", true), generateVideoStream("webm-720_60", MediaFormat.WEBM, "720p60", true),
new VideoStream("", MediaFormat.MPEG_4, /**/ "2160p60", true), generateVideoStream("mpeg_4-2160_60", MediaFormat.MPEG_4, "2160p60", true),
new VideoStream("", MediaFormat.MPEG_4, /**/ "720p60", true), generateVideoStream("mpeg_4-720_60", MediaFormat.MPEG_4, "720p60", true),
new VideoStream("", MediaFormat.MPEG_4, /**/ "1080p", true), generateVideoStream("mpeg_4-1080", MediaFormat.MPEG_4, "1080p", true),
new VideoStream("", MediaFormat.MPEG_4, /**/ "1080p60", true)); generateVideoStream("mpeg_4-1080_60", MediaFormat.MPEG_4, "1080p60", true));
@Test @Test
public void getSortedStreamVideosListTest() { public void getSortedStreamVideosListTest() {
@ -56,7 +59,8 @@ public class ListHelperTest {
assertEquals(expected.size(), result.size()); assertEquals(expected.size(), result.size());
for (int i = 0; i < result.size(); i++) { for (int i = 0; i < result.size(); i++) {
assertEquals(expected.get(i), result.get(i).resolution); assertEquals(result.get(i).getResolution(), expected.get(i));
assertEquals(expected.get(i), result.get(i).getResolution());
} }
//////////////////// ////////////////////
@ -69,7 +73,7 @@ public class ListHelperTest {
"720p", "480p", "360p", "240p", "144p"); "720p", "480p", "360p", "240p", "144p");
assertEquals(expected.size(), result.size()); assertEquals(expected.size(), result.size());
for (int i = 0; i < result.size(); i++) { for (int i = 0; i < result.size(); i++) {
assertEquals(expected.get(i), result.get(i).resolution); assertEquals(expected.get(i), result.get(i).getResolution());
} }
} }
@ -83,8 +87,8 @@ public class ListHelperTest {
assertEquals(expected.size(), result.size()); assertEquals(expected.size(), result.size());
for (int i = 0; i < result.size(); i++) { for (int i = 0; i < result.size(); i++) {
assertEquals(expected.get(i), result.get(i).resolution); assertEquals(expected.get(i), result.get(i).getResolution());
assertTrue(result.get(i).isVideoOnly); assertTrue(result.get(i).isVideoOnly());
} }
////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////
@ -96,8 +100,8 @@ public class ListHelperTest {
expected = Arrays.asList("720p", "480p", "360p", "240p", "144p"); expected = Arrays.asList("720p", "480p", "360p", "240p", "144p");
assertEquals(expected.size(), result.size()); assertEquals(expected.size(), result.size());
for (int i = 0; i < result.size(); i++) { for (int i = 0; i < result.size(); i++) {
assertEquals(expected.get(i), result.get(i).resolution); assertEquals(expected.get(i), result.get(i).getResolution());
assertFalse(result.get(i).isVideoOnly); assertFalse(result.get(i).isVideoOnly());
} }
///////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
@ -113,10 +117,9 @@ public class ListHelperTest {
assertEquals(expected.size(), result.size()); assertEquals(expected.size(), result.size());
for (int i = 0; i < result.size(); i++) { for (int i = 0; i < result.size(); i++) {
assertEquals(expected.get(i), result.get(i).resolution); assertEquals(expected.get(i), result.get(i).getResolution());
assertEquals( assertEquals(expectedVideoOnly.contains(result.get(i).getResolution()),
expectedVideoOnly.contains(result.get(i).resolution), result.get(i).isVideoOnly());
result.get(i).isVideoOnly);
} }
} }
@ -132,66 +135,66 @@ public class ListHelperTest {
"1080p60", "1080p", "720p60", "720p", "480p", "360p", "240p", "144p"); "1080p60", "1080p", "720p60", "720p", "480p", "360p", "240p", "144p");
assertEquals(expected.size(), result.size()); assertEquals(expected.size(), result.size());
for (int i = 0; i < result.size(); i++) { for (int i = 0; i < result.size(); i++) {
assertEquals(expected.get(i), result.get(i).resolution); assertEquals(expected.get(i), result.get(i).getResolution());
} }
} }
@Test @Test
public void getDefaultResolutionTest() { public void getDefaultResolutionTest() {
final List<VideoStream> testList = Arrays.asList( final List<VideoStream> testList = Arrays.asList(
new VideoStream("", MediaFormat.MPEG_4, /**/ "720p"), generateVideoStream("mpeg_4-720", MediaFormat.MPEG_4, "720p", false),
new VideoStream("", MediaFormat.v3GPP, /**/ "240p"), generateVideoStream("v3gpp-240", MediaFormat.v3GPP, "240p", false),
new VideoStream("", MediaFormat.WEBM, /**/ "480p"), generateVideoStream("webm-480", MediaFormat.WEBM, "480p", false),
new VideoStream("", MediaFormat.WEBM, /**/ "240p"), generateVideoStream("webm-240", MediaFormat.WEBM, "240p", false),
new VideoStream("", MediaFormat.MPEG_4, /**/ "240p"), generateVideoStream("mpeg_4-240", MediaFormat.MPEG_4, "240p", false),
new VideoStream("", MediaFormat.WEBM, /**/ "144p"), generateVideoStream("webm-144", MediaFormat.WEBM, "144p", false),
new VideoStream("", MediaFormat.MPEG_4, /**/ "360p"), generateVideoStream("mpeg_4-360", MediaFormat.MPEG_4, "360p", false),
new VideoStream("", MediaFormat.WEBM, /**/ "360p")); generateVideoStream("webm-360", MediaFormat.WEBM, "360p", false));
VideoStream result = testList.get(ListHelper.getDefaultResolutionIndex( VideoStream result = testList.get(ListHelper.getDefaultResolutionIndex(
"720p", BEST_RESOLUTION_KEY, MediaFormat.MPEG_4, testList)); "720p", BEST_RESOLUTION_KEY, MediaFormat.MPEG_4, testList));
assertEquals("720p", result.resolution); assertEquals("720p", result.getResolution());
assertEquals(MediaFormat.MPEG_4, result.getFormat()); assertEquals(MediaFormat.MPEG_4, result.getFormat());
// Have resolution and the format // Have resolution and the format
result = testList.get(ListHelper.getDefaultResolutionIndex( result = testList.get(ListHelper.getDefaultResolutionIndex(
"480p", BEST_RESOLUTION_KEY, MediaFormat.WEBM, testList)); "480p", BEST_RESOLUTION_KEY, MediaFormat.WEBM, testList));
assertEquals("480p", result.resolution); assertEquals("480p", result.getResolution());
assertEquals(MediaFormat.WEBM, result.getFormat()); assertEquals(MediaFormat.WEBM, result.getFormat());
// Have resolution but not the format // Have resolution but not the format
result = testList.get(ListHelper.getDefaultResolutionIndex( result = testList.get(ListHelper.getDefaultResolutionIndex(
"480p", BEST_RESOLUTION_KEY, MediaFormat.MPEG_4, testList)); "480p", BEST_RESOLUTION_KEY, MediaFormat.MPEG_4, testList));
assertEquals("480p", result.resolution); assertEquals("480p", result.getResolution());
assertEquals(MediaFormat.WEBM, result.getFormat()); assertEquals(MediaFormat.WEBM, result.getFormat());
// Have resolution and the format // Have resolution and the format
result = testList.get(ListHelper.getDefaultResolutionIndex( result = testList.get(ListHelper.getDefaultResolutionIndex(
"240p", BEST_RESOLUTION_KEY, MediaFormat.WEBM, testList)); "240p", BEST_RESOLUTION_KEY, MediaFormat.WEBM, testList));
assertEquals("240p", result.resolution); assertEquals("240p", result.getResolution());
assertEquals(MediaFormat.WEBM, result.getFormat()); assertEquals(MediaFormat.WEBM, result.getFormat());
// The best resolution // The best resolution
result = testList.get(ListHelper.getDefaultResolutionIndex( result = testList.get(ListHelper.getDefaultResolutionIndex(
BEST_RESOLUTION_KEY, BEST_RESOLUTION_KEY, MediaFormat.WEBM, testList)); BEST_RESOLUTION_KEY, BEST_RESOLUTION_KEY, MediaFormat.WEBM, testList));
assertEquals("720p", result.resolution); assertEquals("720p", result.getResolution());
assertEquals(MediaFormat.MPEG_4, result.getFormat()); assertEquals(MediaFormat.MPEG_4, result.getFormat());
// Doesn't have the 60fps variant and format // Doesn't have the 60fps variant and format
result = testList.get(ListHelper.getDefaultResolutionIndex( result = testList.get(ListHelper.getDefaultResolutionIndex(
"720p60", BEST_RESOLUTION_KEY, MediaFormat.WEBM, testList)); "720p60", BEST_RESOLUTION_KEY, MediaFormat.WEBM, testList));
assertEquals("720p", result.resolution); assertEquals("720p", result.getResolution());
assertEquals(MediaFormat.MPEG_4, result.getFormat()); assertEquals(MediaFormat.MPEG_4, result.getFormat());
// Doesn't have the 60fps variant // Doesn't have the 60fps variant
result = testList.get(ListHelper.getDefaultResolutionIndex( result = testList.get(ListHelper.getDefaultResolutionIndex(
"480p60", BEST_RESOLUTION_KEY, MediaFormat.WEBM, testList)); "480p60", BEST_RESOLUTION_KEY, MediaFormat.WEBM, testList));
assertEquals("480p", result.resolution); assertEquals("480p", result.getResolution());
assertEquals(MediaFormat.WEBM, result.getFormat()); assertEquals(MediaFormat.WEBM, result.getFormat());
// Doesn't have the resolution, will return the best one // Doesn't have the resolution, will return the best one
result = testList.get(ListHelper.getDefaultResolutionIndex( result = testList.get(ListHelper.getDefaultResolutionIndex(
"2160p60", BEST_RESOLUTION_KEY, MediaFormat.WEBM, testList)); "2160p60", BEST_RESOLUTION_KEY, MediaFormat.WEBM, testList));
assertEquals("720p", result.resolution); assertEquals("720p", result.getResolution());
assertEquals(MediaFormat.MPEG_4, result.getFormat()); assertEquals(MediaFormat.MPEG_4, result.getFormat());
} }
@ -221,8 +224,8 @@ public class ListHelperTest {
//////////////////////////////////////// ////////////////////////////////////////
List<AudioStream> testList = Arrays.asList( List<AudioStream> testList = Arrays.asList(
new AudioStream("", MediaFormat.M4A, /**/ 128), generateAudioStream("m4a-128", MediaFormat.M4A, 128),
new AudioStream("", 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.getHighestQualityAudioIndex(
@ -235,13 +238,13 @@ public class ListHelperTest {
////////////////////////////////////////////////////// //////////////////////////////////////////////////////
testList = new ArrayList<>(Arrays.asList( testList = new ArrayList<>(Arrays.asList(
new AudioStream("", MediaFormat.WEBMA, /**/ 192), generateAudioStream("webma-192-1", MediaFormat.WEBMA, 192),
new AudioStream("", MediaFormat.M4A, /**/ 192), generateAudioStream("m4a-192-1", MediaFormat.M4A, 192),
new AudioStream("", MediaFormat.WEBMA, /**/ 192), generateAudioStream("webma-192-2", MediaFormat.WEBMA, 192),
new AudioStream("", MediaFormat.M4A, /**/ 192), generateAudioStream("m4a-192-2", MediaFormat.M4A, 192),
new AudioStream("", MediaFormat.WEBMA, /**/ 192), generateAudioStream("webma-192-3", MediaFormat.WEBMA, 192),
new AudioStream("", MediaFormat.M4A, /**/ 192), generateAudioStream("m4a-192-3", MediaFormat.M4A, 192),
new AudioStream("", 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.getHighestQualityAudioIndex(MediaFormat.MP3, testList));
@ -250,7 +253,7 @@ public class ListHelperTest {
// 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(new AudioStream("", MediaFormat.WEBMA, /**/ 192)); testList.add(generateAudioStream("webma-192-5", MediaFormat.WEBMA, 192));
stream = testList.get(ListHelper.getHighestQualityAudioIndex(MediaFormat.MP3, testList)); stream = testList.get(ListHelper.getHighestQualityAudioIndex(MediaFormat.MP3, testList));
assertEquals(192, stream.getAverageBitrate()); assertEquals(192, stream.getAverageBitrate());
assertEquals(MediaFormat.M4A, stream.getFormat()); assertEquals(MediaFormat.M4A, stream.getFormat());
@ -288,8 +291,8 @@ public class ListHelperTest {
//////////////////////////////////////// ////////////////////////////////////////
List<AudioStream> testList = new ArrayList<>(Arrays.asList( List<AudioStream> testList = new ArrayList<>(Arrays.asList(
new AudioStream("", MediaFormat.M4A, /**/ 128), generateAudioStream("m4a-128", MediaFormat.M4A, 128),
new AudioStream("", 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.getMostCompactAudioIndex(
@ -298,7 +301,7 @@ public class ListHelperTest {
assertEquals(MediaFormat.M4A, stream.getFormat()); assertEquals(MediaFormat.M4A, stream.getFormat());
// WEBMA is more compact than M4A // WEBMA is more compact than M4A
testList.add(new AudioStream("", MediaFormat.WEBMA, /**/ 128)); testList.add(generateAudioStream("webma-192-2", MediaFormat.WEBMA, 128));
stream = testList.get(ListHelper.getMostCompactAudioIndex(MediaFormat.MP3, testList)); stream = testList.get(ListHelper.getMostCompactAudioIndex(MediaFormat.MP3, testList));
assertEquals(128, stream.getAverageBitrate()); assertEquals(128, stream.getAverageBitrate());
assertEquals(MediaFormat.WEBMA, stream.getFormat()); assertEquals(MediaFormat.WEBMA, stream.getFormat());
@ -308,12 +311,12 @@ public class ListHelperTest {
////////////////////////////////////////////////////// //////////////////////////////////////////////////////
testList = new ArrayList<>(Arrays.asList( testList = new ArrayList<>(Arrays.asList(
new AudioStream("", MediaFormat.WEBMA, /**/ 192), generateAudioStream("webma-192-1", MediaFormat.WEBMA, 192),
new AudioStream("", MediaFormat.M4A, /**/ 192), generateAudioStream("m4a-192-1", MediaFormat.M4A, 192),
new AudioStream("", MediaFormat.WEBMA, /**/ 256), generateAudioStream("webma-256", MediaFormat.WEBMA, 256),
new AudioStream("", MediaFormat.M4A, /**/ 192), generateAudioStream("m4a-192-2", MediaFormat.M4A, 192),
new AudioStream("", MediaFormat.WEBMA, /**/ 192), generateAudioStream("webma-192-2", MediaFormat.WEBMA, 192),
new AudioStream("", 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.getMostCompactAudioIndex(MediaFormat.MP3, testList));
@ -335,14 +338,14 @@ public class ListHelperTest {
@Test @Test
public void getVideoDefaultStreamIndexCombinations() { public void getVideoDefaultStreamIndexCombinations() {
final List<VideoStream> testList = Arrays.asList( final List<VideoStream> testList = Arrays.asList(
new VideoStream("", MediaFormat.MPEG_4, /**/ "1080p"), generateVideoStream("mpeg_4-1080", MediaFormat.MPEG_4, "1080p", false),
new VideoStream("", MediaFormat.MPEG_4, /**/ "720p60"), generateVideoStream("mpeg_4-720_60", MediaFormat.MPEG_4, "720p60", false),
new VideoStream("", MediaFormat.MPEG_4, /**/ "720p"), generateVideoStream("mpeg_4-720", MediaFormat.MPEG_4, "720p", false),
new VideoStream("", MediaFormat.WEBM, /**/ "480p"), generateVideoStream("webm-480", MediaFormat.WEBM, "480p", false),
new VideoStream("", MediaFormat.MPEG_4, /**/ "360p"), generateVideoStream("mpeg_4-360", MediaFormat.MPEG_4, "360p", false),
new VideoStream("", MediaFormat.WEBM, /**/ "360p"), generateVideoStream("webm-360", MediaFormat.WEBM, "360p", false),
new VideoStream("", MediaFormat.v3GPP, /**/ "240p60"), generateVideoStream("v3gpp-240_60", MediaFormat.v3GPP, "240p60", false),
new VideoStream("", MediaFormat.WEBM, /**/ "144p")); generateVideoStream("webm-144", MediaFormat.WEBM, "144p", false));
// exact matches // exact matches
assertEquals(1, ListHelper.getVideoStreamIndex("720p60", MediaFormat.MPEG_4, testList)); assertEquals(1, ListHelper.getVideoStreamIndex("720p60", MediaFormat.MPEG_4, testList));
@ -375,4 +378,30 @@ public class ListHelperTest {
// Can't find a match // Can't find a match
assertEquals(-1, ListHelper.getVideoStreamIndex("100p", null, testList)); assertEquals(-1, ListHelper.getVideoStreamIndex("100p", null, testList));
} }
@NonNull
private static AudioStream generateAudioStream(@NonNull final String id,
@Nullable final MediaFormat mediaFormat,
final int averageBitrate) {
return new AudioStream.Builder()
.setId(id)
.setContent("", true)
.setMediaFormat(mediaFormat)
.setAverageBitrate(averageBitrate)
.build();
}
@NonNull
private static VideoStream generateVideoStream(@NonNull final String id,
@Nullable final MediaFormat mediaFormat,
@NonNull final String resolution,
final boolean isVideoOnly) {
return new VideoStream.Builder()
.setId(id)
.setContent("", true)
.setIsVideoOnly(isVideoOnly)
.setResolution(resolution)
.setMediaFormat(mediaFormat)
.build();
}
} }