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
// This works thanks to JitPack: https://jitpack.io/
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
implementation 'com.github.TeamNewPipe:NewPipeExtractor:ac1c22d81c65b7b0c5427f4e1989f5256d617f32'
implementation 'com.github.TeamNewPipe:NewPipeExtractor:1b51eab664ec7cbd2295c96d8b43000379cd1b7b'
/** Checkstyle **/
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"

View File

@ -91,7 +91,12 @@ class StreamItemAdapterTest {
context,
StreamItemAdapter.StreamSizeWrapper(
(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
),
@ -108,7 +113,14 @@ class StreamItemAdapterTest {
val adapter = StreamItemAdapter<AudioStream, Stream>(
context,
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
),
null
@ -126,7 +138,13 @@ class StreamItemAdapterTest {
private fun getVideoStreams(vararg videoOnly: Boolean) =
StreamItemAdapter.StreamSizeWrapper(
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
)
@ -138,8 +156,16 @@ class StreamItemAdapterTest {
private fun getAudioStreams(vararg shouldBeValid: Boolean) =
getSecondaryStreamsFromList(
shouldBeValid.map {
if (it) AudioStream("https://example.com", MediaFormat.OPUS, 192)
else null
if (it) {
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.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.player.MainPlayer;
@ -677,22 +676,15 @@ public class RouterActivity extends AppCompatActivity {
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {
final List<VideoStream> sortedVideoStreams = ListHelper
.getSortedStreamVideosList(this, result.getVideoStreams(),
result.getVideoOnlyStreams(), false, false);
final int selectedVideoStreamIndex = ListHelper
.getDefaultResolutionIndex(this, sortedVideoStreams);
final DownloadDialog downloadDialog = DownloadDialog.newInstance(this, result);
downloadDialog.setSelectedVideoStream(ListHelper.getDefaultResolutionIndex(
this, downloadDialog.wrappedVideoStreams.getStreamsList()));
downloadDialog.setOnDismissListener(dialog -> finish());
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");
fm.executePendingTransactions();
}, throwable ->
showUnsupportedUrlDialog(currentUrl)));
}, throwable -> showUnsupportedUrlDialog(currentUrl)));
}
@Override

View File

@ -48,6 +48,7 @@ import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.localization.Localization;
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.SubtitlesStream;
@ -71,6 +72,7 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import icepick.Icepick;
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.MissionState;
import static org.schabi.newpipe.util.ListHelper.keepStreamsWithDelivery;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
public class DownloadDialog extends DialogFragment
@ -92,11 +95,11 @@ public class DownloadDialog extends DialogFragment
@State
StreamInfo currentInfo;
@State
StreamSizeWrapper<AudioStream> wrappedAudioStreams = StreamSizeWrapper.empty();
public StreamSizeWrapper<AudioStream> wrappedAudioStreams = StreamSizeWrapper.empty();
@State
StreamSizeWrapper<VideoStream> wrappedVideoStreams = StreamSizeWrapper.empty();
public StreamSizeWrapper<VideoStream> wrappedVideoStreams = StreamSizeWrapper.empty();
@State
StreamSizeWrapper<SubtitlesStream> wrappedSubtitleStreams = StreamSizeWrapper.empty();
public StreamSizeWrapper<SubtitlesStream> wrappedSubtitleStreams = StreamSizeWrapper.empty();
@State
int selectedVideoIndex = 0;
@State
@ -138,28 +141,39 @@ public class DownloadDialog extends DialogFragment
registerForActivityResult(
new StartActivityForResult(), this::requestDownloadPickVideoFolderResult);
/*//////////////////////////////////////////////////////////////////////////
// Instance creation
//////////////////////////////////////////////////////////////////////////*/
public static DownloadDialog newInstance(final StreamInfo info) {
final DownloadDialog dialog = new DownloadDialog();
dialog.setInfo(info);
return dialog;
}
@NonNull
public static DownloadDialog newInstance(final Context context,
@NonNull final StreamInfo info) {
// 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 ArrayList<VideoStream> streamsList = new ArrayList<>(ListHelper
.getSortedStreamVideosList(context, info.getVideoStreams(),
info.getVideoOnlyStreams(), false, false));
final int selectedStreamIndex = ListHelper.getDefaultResolutionIndex(context, streamsList);
final List<VideoStream> videoOnlyStreams = new ArrayList<>(info.getVideoOnlyStreams());
final List<VideoStream> progressiveHttpVideoOnlyStreams =
keepStreamsWithDelivery(videoOnlyStreams, DeliveryMethod.PROGRESSIVE_HTTP);
final DownloadDialog instance = newInstance(info);
instance.setVideoStreams(streamsList);
instance.setSelectedVideoStream(selectedStreamIndex);
instance.setAudioStreams(info.getAudioStreams());
instance.setSubtitleStreams(info.getSubtitles());
final List<AudioStream> audioStreams = new ArrayList<>(info.getAudioStreams());
final List<AudioStream> progressiveHttpAudioStreams =
keepStreamsWithDelivery(audioStreams, DeliveryMethod.PROGRESSIVE_HTTP);
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;
}
@ -169,45 +183,69 @@ public class DownloadDialog extends DialogFragment
// Setters
//////////////////////////////////////////////////////////////////////////*/
private void setInfo(final StreamInfo info) {
private void setInfo(@NonNull final StreamInfo info) {
this.currentInfo = info;
}
public void setAudioStreams(final List<AudioStream> audioStreams) {
setAudioStreams(new StreamSizeWrapper<>(audioStreams, getContext()));
public void setAudioStreams(@NonNull final List<AudioStream> audioStreams) {
this.wrappedAudioStreams = new StreamSizeWrapper<>(audioStreams, getContext());
}
public void setAudioStreams(final StreamSizeWrapper<AudioStream> was) {
this.wrappedAudioStreams = was;
public void setVideoStreams(@NonNull final List<VideoStream> videoStreams) {
this.wrappedVideoStreams = new StreamSizeWrapper<>(videoStreams, getContext());
}
public void setVideoStreams(final List<VideoStream> videoStreams) {
setVideoStreams(new StreamSizeWrapper<>(videoStreams, 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;
public void setSubtitleStreams(@NonNull final List<SubtitlesStream> subtitleStreams) {
this.wrappedSubtitleStreams = new StreamSizeWrapper<>(subtitleStreams, getContext());
}
/**
* 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) {
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) {
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) {
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) {
@ -249,11 +287,16 @@ public class DownloadDialog extends DialogFragment
.getAudioStreamFor(wrappedAudioStreams.getStreamsList(), videoStreams.get(i));
if (audioStream != null) {
secondaryStreams
.append(i, new SecondaryStreamHelper<>(wrappedAudioStreams, audioStream));
secondaryStreams.append(i, new SecondaryStreamHelper<>(wrappedAudioStreams,
audioStream));
} else if (DEBUG) {
Log.w(TAG, "No audio stream candidates for video format "
+ videoStreams.get(i).getFormat().name());
final MediaFormat mediaFormat = videoStreams.get(i).getFormat();
if (mediaFormat != null) {
Log.w(TAG, "No audio stream candidates for video format "
+ mediaFormat.name());
} else {
Log.w(TAG, "No audio stream candidates for unknown video format");
}
}
}
@ -288,7 +331,8 @@ public class DownloadDialog extends DialogFragment
}
@Override
public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container,
public View onCreateView(@NonNull final LayoutInflater inflater,
final ViewGroup container,
final Bundle savedInstanceState) {
if (DEBUG) {
Log.d(TAG, "onCreateView() called with: "
@ -299,14 +343,15 @@ public class DownloadDialog extends DialogFragment
}
@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);
dialogBinding = DownloadDialogBinding.bind(view);
dialogBinding.fileName.setText(FilenameUtils.createFilename(getContext(),
currentInfo.getName()));
selectedAudioIndex = ListHelper
.getDefaultAudioFormat(getContext(), currentInfo.getAudioStreams());
.getDefaultAudioFormat(getContext(), wrappedAudioStreams.getStreamsList());
selectedSubtitleIndex = getSubtitleIndexBy(subtitleStreamsAdapter.getAll());
@ -324,7 +369,8 @@ public class DownloadDialog extends DialogFragment
dialogBinding.threads.setProgress(threads - 1);
dialogBinding.threads.setOnSeekBarChangeListener(new SimpleOnSeekBarChangeListener() {
@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 int newProgress = progress + 1;
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);
}
private void requestDownloadSaveAsResult(final ActivityResult result) {
private void requestDownloadSaveAsResult(@NonNull final ActivityResult result) {
if (result.getResultCode() != Activity.RESULT_OK) {
return;
}
@ -486,8 +532,8 @@ public class DownloadDialog extends DialogFragment
return;
}
final DocumentFile docFile
= DocumentFile.fromSingleUri(context, result.getData().getData());
final DocumentFile docFile = DocumentFile.fromSingleUri(context,
result.getData().getData());
if (docFile == null) {
showFailedDialog(R.string.general_error);
return;
@ -498,7 +544,7 @@ public class DownloadDialog extends DialogFragment
docFile.getType());
}
private void requestDownloadPickFolderResult(final ActivityResult result,
private void requestDownloadPickFolderResult(@NonNull final ActivityResult result,
final String key,
final String tag) {
if (result.getResultCode() != Activity.RESULT_OK) {
@ -518,12 +564,11 @@ public class DownloadDialog extends DialogFragment
StoredDirectoryHelper.PERMISSION_FLAGS);
}
PreferenceManager.getDefaultSharedPreferences(context).edit()
.putString(key, uri.toString()).apply();
PreferenceManager.getDefaultSharedPreferences(context).edit().putString(key,
uri.toString()).apply();
try {
final StoredDirectoryHelper mainStorage
= new StoredDirectoryHelper(context, uri, tag);
final StoredDirectoryHelper mainStorage = new StoredDirectoryHelper(context, uri, tag);
checkSelectedDownload(mainStorage, mainStorage.findFile(filenameTmp),
filenameTmp, mimeTmp);
} catch (final IOException e) {
@ -561,8 +606,10 @@ public class DownloadDialog extends DialogFragment
}
@Override
public void onItemSelected(final AdapterView<?> parent, final View view,
final int position, final long id) {
public void onItemSelected(final AdapterView<?> parent,
final View view,
final int position,
final long id) {
if (DEBUG) {
Log.d(TAG, "onItemSelected() called with: "
+ "parent = [" + parent + "], view = [" + view + "], "
@ -597,14 +644,16 @@ public class DownloadDialog extends DialogFragment
final boolean isAudioStreamsAvailable = audioStreamsAdapter.getCount() > 0;
final boolean isSubtitleStreamsAvailable = subtitleStreamsAdapter.getCount() > 0;
dialogBinding.audioButton.setVisibility(isAudioStreamsAvailable ? View.VISIBLE : View.GONE);
dialogBinding.videoButton.setVisibility(isVideoStreamsAvailable ? View.VISIBLE : View.GONE);
dialogBinding.audioButton.setVisibility(isAudioStreamsAvailable ? View.VISIBLE
: View.GONE);
dialogBinding.videoButton.setVisibility(isVideoStreamsAvailable ? View.VISIBLE
: View.GONE);
dialogBinding.subtitleButton.setVisibility(isSubtitleStreamsAvailable
? View.VISIBLE : View.GONE);
prefs = PreferenceManager.getDefaultSharedPreferences(requireContext());
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
&& (defaultMedia.equals(getString(R.string.last_download_type_video_key)))) {
@ -640,7 +689,7 @@ public class DownloadDialog extends DialogFragment
dialogBinding.subtitleButton.setEnabled(enabled);
}
private int getSubtitleIndexBy(final List<SubtitlesStream> streams) {
private int getSubtitleIndexBy(@NonNull final List<SubtitlesStream> streams) {
final Localization preferredLocalization = NewPipe.getPreferredLocalization();
int candidate = 0;
@ -666,8 +715,10 @@ public class DownloadDialog extends DialogFragment
return candidate;
}
@NonNull
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);
}
@ -683,12 +734,8 @@ public class DownloadDialog extends DialogFragment
}
private void launchDirectoryPicker(final ActivityResultLauncher<Intent> launcher) {
NoFileManagerSafeGuard.launchSafe(
launcher,
StoredDirectoryHelper.getPicker(context),
TAG,
context
);
NoFileManagerSafeGuard.launchSafe(launcher, StoredDirectoryHelper.getPicker(context), TAG,
context);
}
private void prepareSelectedDownload() {
@ -710,30 +757,46 @@ public class DownloadDialog extends DialogFragment
mimeTmp = "audio/ogg";
filenameTmp += "opus";
} else {
mimeTmp = format.mimeType;
filenameTmp += format.suffix;
if (format != null) {
mimeTmp = format.mimeType;
}
if (format != null) {
filenameTmp += format.suffix;
}
}
break;
case R.id.video_button:
selectedMediaType = getString(R.string.last_download_type_video_key);
mainStorage = mainStorageVideo;
format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
mimeTmp = format.mimeType;
filenameTmp += format.suffix;
if (format != null) {
mimeTmp = format.mimeType;
}
if (format != null) {
filenameTmp += format.suffix;
}
break;
case R.id.subtitle_button:
selectedMediaType = getString(R.string.last_download_type_subtitle_key);
mainStorage = mainStorageVideo; // subtitle & video files go together
format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat();
mimeTmp = format.mimeType;
filenameTmp += (format == MediaFormat.TTML ? MediaFormat.SRT : format).suffix;
if (format != null) {
mimeTmp = format.mimeType;
}
if (format == MediaFormat.TTML) {
filenameTmp += MediaFormat.SRT.suffix;
} else {
if (format != null) {
filenameTmp += format.suffix;
}
}
break;
default:
throw new RuntimeException("No stream selected");
}
if (!askForSavePath
&& (mainStorage == null
if (!askForSavePath && (mainStorage == null
|| mainStorage.isDirect() == NewPipeSettings.useStorageAccessFramework(context)
|| mainStorage.isInvalidSafStorage())) {
// Pick new download folder if one of:
@ -767,18 +830,16 @@ public class DownloadDialog extends DialogFragment
initialPath = Uri.parse(initialSavePath.getAbsolutePath());
}
NoFileManagerSafeGuard.launchSafe(
requestDownloadSaveAsLauncher,
StoredFileHelper.getNewPicker(context, filenameTmp, mimeTmp, initialPath),
TAG,
context
);
NoFileManagerSafeGuard.launchSafe(requestDownloadSaveAsLauncher,
StoredFileHelper.getNewPicker(context, filenameTmp, mimeTmp, initialPath), TAG,
context);
return;
}
// 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
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,
final Uri targetFile, final String filename,
final Uri targetFile,
final String filename,
final String mime) {
StoredFileHelper storage;
@ -947,7 +1009,7 @@ public class DownloadDialog extends DialogFragment
storage.truncate();
}
} 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);
return;
}
@ -992,8 +1054,8 @@ public class DownloadDialog extends DialogFragment
}
psArgs = null;
final long videoSize = wrappedVideoStreams
.getSizeInBytes((VideoStream) selectedStream);
final long videoSize = wrappedVideoStreams.getSizeInBytes(
(VideoStream) selectedStream);
// 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
@ -1009,7 +1071,7 @@ public class DownloadDialog extends DialogFragment
if (selectedStream.getFormat() == MediaFormat.TTML) {
psName = Postprocessing.ALGORITHM_TTML_CONVERTER;
psArgs = new String[]{
psArgs = new String[] {
selectedStream.getFormat().getSuffix(),
"false" // ignore empty frames
};
@ -1020,17 +1082,22 @@ public class DownloadDialog extends DialogFragment
}
if (secondaryStream == null) {
urls = new String[]{
selectedStream.getUrl()
urls = new String[] {
selectedStream.getContent()
};
recoveryInfo = new MissionRecoveryInfo[]{
recoveryInfo = new MissionRecoveryInfo[] {
new MissionRecoveryInfo(selectedStream)
};
} else {
urls = new String[]{
selectedStream.getUrl(), secondaryStream.getUrl()
if (secondaryStream.getDeliveryMethod() != DeliveryMethod.PROGRESSIVE_HTTP) {
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)};
}

View File

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

View File

@ -96,9 +96,10 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
case VIDEO_STREAM:
case LIVE_STREAM:
case AUDIO_LIVE_STREAM:
case POST_LIVE_STREAM:
case POST_LIVE_AUDIO_STREAM:
enableLongClick(item);
break;
case FILE:
case NONE:
default:
disableLongClick();
@ -114,7 +115,8 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
final StreamStateEntity state
= historyRecordManager.loadStreamState(infoItem).blockingGet()[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());
if (itemProgressView.getVisibility() == View.VISIBLE) {
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_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.util.Localization
import org.schabi.newpipe.util.PicassoHelper
@ -109,7 +111,7 @@ data class StreamItem(
}
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
}

View File

@ -1744,24 +1744,9 @@ public final class Player implements
if (exoPlayerIsNull()) {
return;
}
// Use duration of currentItem for non-live streams,
// because HLS streams are fragmented
// and thus the whole duration is not available to the player
// 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()
);
onUpdateProgress(Math.max((int) simpleExoPlayer.getCurrentPosition(), 0),
(int) simpleExoPlayer.getDuration(), simpleExoPlayer.getBufferedPercentage());
}
private Disposable getProgressUpdateDisposable() {
@ -3399,6 +3384,7 @@ public final class Player implements
switch (info.getStreamType()) {
case AUDIO_STREAM:
case POST_LIVE_AUDIO_STREAM:
binding.surfaceView.setVisibility(View.GONE);
binding.endScreen.setVisibility(View.VISIBLE);
binding.playbackEndTime.setVisibility(View.VISIBLE);
@ -3417,6 +3403,7 @@ public final class Player implements
break;
case VIDEO_STREAM:
case POST_LIVE_STREAM:
if (currentMetadata == null
|| !currentMetadata.getMaybeQuality().isPresent()
|| (info.getVideoStreams().isEmpty()
@ -3484,10 +3471,10 @@ public final class Player implements
for (int i = 0; i < availableStreams.size(); i++) {
final VideoStream videoStream = availableStreams.get(i);
qualityPopupMenu.getMenu().add(POPUP_MENU_ID_QUALITY, i, Menu.NONE, MediaFormat
.getNameById(videoStream.getFormatId()) + " " + videoStream.resolution);
.getNameById(videoStream.getFormatId()) + " " + videoStream.getResolution());
}
if (getSelectedVideoStream() != null) {
binding.qualityTextView.setText(getSelectedVideoStream().resolution);
binding.qualityTextView.setText(getSelectedVideoStream().getResolution());
}
qualityPopupMenu.setOnMenuItemClickListener(this);
qualityPopupMenu.setOnDismissListener(this);
@ -3605,7 +3592,7 @@ public final class Player implements
}
saveStreamProgressState(); //TODO added, check if good
final String newResolution = availableStreams.get(menuItemIndex).resolution;
final String newResolution = availableStreams.get(menuItemIndex).getResolution();
setRecovery();
setPlaybackQuality(newResolution);
reloadPlayQueueManager();
@ -3633,7 +3620,7 @@ public final class Player implements
}
isSomePopupMenuVisible = false; //TODO check if this works
if (getSelectedVideoStream() != null) {
binding.qualityTextView.setText(getSelectedVideoStream().resolution);
binding.qualityTextView.setText(getSelectedVideoStream().getResolution());
}
if (isPlaying()) {
hideControls(DEFAULT_CONTROLS_DURATION, 0);
@ -4250,7 +4237,8 @@ public final class Player implements
} else {
final StreamType streamType = info.getStreamType();
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
setRecovery();
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:
*
* <ul>
* <li>the content is an {@link StreamType#AUDIO_STREAM audio stream} or an
* {@link StreamType#AUDIO_LIVE_STREAM audio live stream};</li>
* <li>the content is an {@link StreamType#AUDIO_STREAM audio stream}, an
* {@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
* {@link SourceType#LIVE_STREAM live source};</li>
* <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
* {@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}.
* </li>
* </ul>
@ -4309,14 +4299,17 @@ public final class Player implements
final StreamType streamType = streamInfo.getStreamType();
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;
}
// 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
// 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
&& sourceType == SourceType.LIVE_STREAM)) {
return false;
@ -4331,8 +4324,10 @@ public final class Player implements
|| (sourceType == SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY
&& isNullOrEmpty(streamInfo.getAudioStreams()))) {
// 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
return streamType != StreamType.VIDEO_STREAM && streamType != StreamType.LIVE_STREAM;
// is a video stream, a live stream or an ended 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

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.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.database.StandaloneDatabaseProvider;
import com.google.android.exoplayer2.upstream.DataSource;
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.SimpleCache;
import org.schabi.newpipe.player.datasource.YoutubeHttpDataSource;
import java.io.File;
import androidx.annotation.NonNull;
/* package-private */ class CacheFactory implements DataSource.Factory {
private static final String TAG = "CacheFactory";
/* package-private */ final class CacheFactory implements DataSource.Factory {
private static final String TAG = CacheFactory.class.getSimpleName();
private static final String CACHE_FOLDER_NAME = "exoplayer";
private static final int CACHE_FLAGS = CacheDataSource.FLAG_BLOCK_ON_CACHE
| 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 final int CACHE_FLAGS = CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR;
private static SimpleCache cache;
CacheFactory(@NonNull final Context context,
@NonNull final String userAgent,
@NonNull final TransferListener transferListener) {
this(context, userAgent, transferListener, PlayerHelper.getPreferredCacheSize(),
PlayerHelper.getPreferredFileSize());
private final long maxFileSize;
private final Context context;
private final String userAgent;
private final TransferListener transferListener;
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,
@NonNull final String userAgent,
@NonNull final TransferListener transferListener,
final long maxCacheSize,
final long maxFileSize) {
this.maxFileSize = maxFileSize;
@Nullable final DataSource.Factory upstreamDataSourceFactory) {
this.context = context;
this.userAgent = userAgent;
this.transferListener = transferListener;
this.upstreamDataSourceFactory = upstreamDataSourceFactory;
dataSourceFactory = new DefaultDataSource
.Factory(context, new DefaultHttpDataSource.Factory().setUserAgent(userAgent))
.setTransferListener(transferListener);
cacheDir = new File(context.getExternalCacheDir(), CACHE_FOLDER_NAME);
final File cacheDir = new File(context.getExternalCacheDir(), CACHE_FOLDER_NAME);
if (!cacheDir.exists()) {
//noinspection ResultOfMethodCallIgnored
cacheDir.mkdir();
@ -60,37 +76,43 @@ import androidx.annotation.NonNull;
if (cache == null) {
final LeastRecentlyUsedCacheEvictor evictor
= new LeastRecentlyUsedCacheEvictor(maxCacheSize);
= new LeastRecentlyUsedCacheEvictor(PlayerHelper.getPreferredCacheSize());
cache = new SimpleCache(cacheDir, evictor, new StandaloneDatabaseProvider(context));
Log.d(TAG, "initExoPlayerCache: cacheDir = " + cacheDir.getAbsolutePath());
}
maxFileSize = PlayerHelper.getPreferredFileSize();
}
@NonNull
@Override
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 CacheDataSink dataSink = new CacheDataSink(cache, maxFileSize);
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 androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.source.SingleSampleMediaSource;
import com.google.android.exoplayer2.source.dash.DashMediaSource;
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
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.HlsPlaylistParserFactory;
import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource;
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSource;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
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 {
@ -29,79 +35,120 @@ public class PlayerDataSource {
* early.
*/
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 DataSource.Factory cacheDataSourceFactory;
private final CacheFactory.Builder cacheDataSourceFactoryBuilder;
private final DataSource.Factory cachelessDataSourceFactory;
public PlayerDataSource(@NonNull final Context context,
@NonNull final String userAgent,
@NonNull final TransferListener transferListener) {
continueLoadingCheckIntervalBytes = PlayerHelper.getProgressiveLoadIntervalBytes(context);
cacheDataSourceFactory = new CacheFactory(context, userAgent, transferListener);
cachelessDataSourceFactory = new DefaultDataSource
.Factory(context, new DefaultHttpDataSource.Factory().setUserAgent(userAgent))
cacheDataSourceFactoryBuilder = new CacheFactory.Builder(context, userAgent,
transferListener);
cachelessDataSourceFactory = new DefaultDataSource.Factory(context,
new DefaultHttpDataSource.Factory().setUserAgent(userAgent))
.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() {
return new SsMediaSource.Factory(
new DefaultSsChunkSource.Factory(cachelessDataSourceFactory),
cachelessDataSourceFactory
)
.setLoadErrorHandlingPolicy(
new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY))
.setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS);
return getSSMediaSourceFactory().setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS);
}
public HlsMediaSource.Factory getLiveHlsMediaSourceFactory() {
return new HlsMediaSource.Factory(cachelessDataSourceFactory)
.setAllowChunklessPreparation(true)
.setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(
MANIFEST_MINIMUM_RETRY))
.setPlaylistTrackerFactory((dataSourceFactory, loadErrorHandlingPolicy,
playlistParserFactory) ->
new DefaultHlsPlaylistTracker(dataSourceFactory, loadErrorHandlingPolicy,
playlistParserFactory, PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT)
);
playlistParserFactory,
PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT));
}
public DashMediaSource.Factory getLiveDashMediaSourceFactory() {
return new DashMediaSource.Factory(
getDefaultDashChunkSourceFactory(cachelessDataSourceFactory),
cachelessDataSourceFactory
)
.setLoadErrorHandlingPolicy(
new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY));
cachelessDataSourceFactory);
}
private DefaultDashChunkSource.Factory getDefaultDashChunkSourceFactory(
final DataSource.Factory dataSourceFactory
) {
return new DefaultDashChunkSource.Factory(dataSourceFactory);
}
public HlsMediaSource.Factory getHlsMediaSourceFactory() {
return new HlsMediaSource.Factory(cacheDataSourceFactory);
public HlsMediaSource.Factory getHlsMediaSourceFactory(
@Nullable final HlsPlaylistParserFactory hlsPlaylistParserFactory) {
final HlsMediaSource.Factory factory = new HlsMediaSource.Factory(
cacheDataSourceFactoryBuilder.build());
if (hlsPlaylistParserFactory != null) {
factory.setPlaylistParserFactory(hlsPlaylistParserFactory);
}
return factory;
}
public DashMediaSource.Factory getDashMediaSourceFactory() {
return new DashMediaSource.Factory(
getDefaultDashChunkSourceFactory(cacheDataSourceFactory),
cacheDataSourceFactory
);
getDefaultDashChunkSourceFactory(cacheDataSourceFactoryBuilder.build()),
cacheDataSourceFactoryBuilder.build());
}
public ProgressiveMediaSource.Factory getExtractorMediaSourceFactory() {
return new ProgressiveMediaSource.Factory(cacheDataSourceFactory)
.setContinueLoadingCheckIntervalBytes(continueLoadingCheckIntervalBytes)
.setLoadErrorHandlingPolicy(
new DefaultLoadErrorHandlingPolicy(EXTRACTOR_MINIMUM_RETRY));
public ProgressiveMediaSource.Factory getProgressiveMediaSourceFactory() {
return new ProgressiveMediaSource.Factory(cacheDataSourceFactoryBuilder.build())
.setContinueLoadingCheckIntervalBytes(continueLoadingCheckIntervalBytes);
}
public SingleSampleMediaSource.Factory getSampleMediaSourceFactory() {
return new SingleSampleMediaSource.Factory(cacheDataSourceFactory);
public SsMediaSource.Factory getSSMediaSourceFactory() {
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_OFF;
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.PLAYER_TYPE;
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;
}
private PlayerHelper() { }
private PlayerHelper() {
}
////////////////////////////////////////////////////////////////////////////
// Exposed helpers
////////////////////////////////////////////////////////////////////////////
@NonNull
public static String getTimeString(final int milliSeconds) {
final int seconds = (milliSeconds % 60000) / 1000;
final int minutes = (milliSeconds % 3600000) / 60000;
@ -131,15 +135,18 @@ public final class PlayerHelper {
).toString();
}
@NonNull
public static String formatSpeed(final double speed) {
return SPEED_FORMATTER.format(speed);
}
@NonNull
public static String formatPitch(final double pitch) {
return PITCH_FORMATTER.format(pitch);
}
public static String subtitleMimeTypesOf(final MediaFormat format) {
@NonNull
public static String subtitleMimeTypesOf(@NonNull final MediaFormat format) {
switch (format) {
case VTT:
return MimeTypes.TEXT_VTT;
@ -192,14 +199,48 @@ public final class PlayerHelper {
@NonNull
public static String cacheKeyOf(@NonNull final StreamInfo info,
@NonNull final VideoStream video) {
return info.getUrl() + video.getResolution() + video.getFormat().getName();
@NonNull final VideoStream videoStream) {
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
public static String cacheKeyOf(@NonNull final StreamInfo info,
@NonNull final AudioStream audio) {
return info.getUrl() + audio.getAverageBitrate() + audio.getFormat().getName();
@NonNull final AudioStream audioStream) {
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;
}
if (relatedItems.get(0) != null && relatedItems.get(0) instanceof StreamInfoItem
if (relatedItems.get(0) instanceof StreamInfoItem
&& !urls.contains(relatedItems.get(0).getUrl())) {
return getAutoQueuedSinglePlayQueue((StreamInfoItem) relatedItems.get(0));
}
@ -335,6 +376,7 @@ public final class PlayerHelper {
return 2 * 1024 * 1024L; // ExoPlayer CacheDataSink.MIN_RECOMMENDED_FRAGMENT_SIZE
}
@NonNull
public static ExoTrackSelection.Factory getQualitySelector() {
return new AdaptiveTrackSelection.Factory(
1000,
@ -389,7 +431,7 @@ public final class PlayerHelper {
/**
* @param context the Android context
* @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) {
final SharedPreferences sp = getPreferences(context);
@ -480,7 +522,8 @@ public final class PlayerHelper {
return REPEAT_MODE_ONE;
case REPEAT_MODE_ONE:
return REPEAT_MODE_ALL;
case REPEAT_MODE_ALL: default:
case REPEAT_MODE_ALL:
default:
return REPEAT_MODE_OFF;
}
}
@ -548,7 +591,7 @@ public final class PlayerHelper {
player.getContext().getResources().getDimension(R.dimen.popup_default_width);
final float popupWidth = popupRememberSizeAndPos
? player.getPrefs().getFloat(player.getContext().getString(
R.string.popup_saved_width_key), defaultSize)
R.string.popup_saved_width_key), defaultSize)
: defaultSize;
final float popupHeight = getMinimumVideoHeight(popupWidth);
@ -564,10 +607,10 @@ public final class PlayerHelper {
final int centerY = (int) (player.getScreenHeight() / 2f - popupHeight / 2f);
popupLayoutParams.x = popupRememberSizeAndPos
? 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
? player.getPrefs().getInt(player.getContext().getString(
R.string.popup_saved_y_key), centerY) : centerY;
R.string.popup_saved_y_key), centerY) : centerY;
return popupLayoutParams;
}

View File

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

View File

@ -1,13 +1,15 @@
package org.schabi.newpipe.player.resolver;
import static org.schabi.newpipe.util.ListHelper.removeTorrentStreams;
import android.content.Context;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
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.StreamInfo;
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.util.ListHelper;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class AudioPlaybackResolver implements PlaybackResolver {
private static final String TAG = AudioPlaybackResolver.class.getSimpleName();
@NonNull
private final Context context;
@NonNull
@ -31,19 +39,28 @@ public class AudioPlaybackResolver implements PlaybackResolver {
@Override
@Nullable
public MediaSource resolve(@NonNull final StreamInfo info) {
final MediaSource liveSource = maybeBuildLiveMediaSource(dataSource, info);
final MediaSource liveSource = PlaybackResolver.maybeBuildLiveMediaSource(dataSource, info);
if (liveSource != null) {
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()) {
return null;
}
final AudioStream audio = info.getAudioStreams().get(index);
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;
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.text.TextUtils;
import android.util.Log;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.MediaItem;
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.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.mediaitem.MediaItemTag;
import org.schabi.newpipe.player.mediaitem.StreamInfoTag;
@ -18,13 +41,17 @@ import org.schabi.newpipe.util.StreamTypeUtil;
import androidx.annotation.NonNull;
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> {
String TAG = PlaybackResolver.class.getSimpleName();
@Nullable
default MediaSource maybeBuildLiveMediaSource(@NonNull final PlayerDataSource dataSource,
@NonNull final StreamInfo info) {
static MediaSource maybeBuildLiveMediaSource(@NonNull final PlayerDataSource dataSource,
@NonNull final StreamInfo info) {
final StreamType streamType = info.getStreamType();
if (!StreamTypeUtil.isLiveStream(streamType)) {
return null;
@ -41,10 +68,10 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
}
@NonNull
default MediaSource buildLiveMediaSource(@NonNull final PlayerDataSource dataSource,
@NonNull final String sourceUrl,
@C.ContentType final int type,
@NonNull final MediaItemTag metadata) {
static MediaSource buildLiveMediaSource(@NonNull final PlayerDataSource dataSource,
@NonNull final String sourceUrl,
@C.ContentType final int type,
@NonNull final MediaItemTag metadata) {
final MediaSource.Factory factory;
switch (type) {
case C.TYPE_SS:
@ -67,46 +94,342 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
.setLiveConfiguration(
new MediaItem.LiveConfiguration.Builder()
.setTargetOffsetMs(LIVE_STREAM_EDGE_GAP_MILLIS)
.build()
)
.build()
);
.build())
.build());
}
@NonNull
default MediaSource buildMediaSource(@NonNull final PlayerDataSource dataSource,
@NonNull final String sourceUrl,
@NonNull final String cacheKey,
@NonNull final String overrideExtension,
@NonNull final MediaItemTag metadata) {
final Uri uri = Uri.parse(sourceUrl);
@C.ContentType final int type = TextUtils.isEmpty(overrideExtension)
? Util.inferContentType(uri) : Util.inferContentType("." + overrideExtension);
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);
static MediaSource buildMediaSource(@NonNull final PlayerDataSource dataSource,
@NonNull final Stream stream,
@NonNull final StreamInfo streamInfo,
@NonNull final String cacheKey,
@NonNull final MediaItemTag metadata)
throws IOException {
if (streamInfo.getService() == ServiceList.YouTube) {
return createYoutubeMediaSource(stream, streamInfo, dataSource, cacheKey, metadata);
}
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()
.setTag(metadata)
.setUri(uri)
.setCustomCacheKey(cacheKey)
.build()
);
.setTag(metadata)
.setUri(Uri.parse(stream.getContent()))
.setCustomCacheKey(cacheKey)
.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.net.Uri;
import android.util.Log;
import androidx.annotation.NonNull;
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.util.ListHelper;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
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 {
private static final String TAG = VideoPlaybackResolver.class.getSimpleName();
@NonNull
private final Context context;
@NonNull
@ -57,17 +63,22 @@ public class VideoPlaybackResolver implements PlaybackResolver {
@Override
@Nullable
public MediaSource resolve(@NonNull final StreamInfo info) {
final MediaSource liveSource = maybeBuildLiveMediaSource(dataSource, info);
final MediaSource liveSource = PlaybackResolver.maybeBuildLiveMediaSource(dataSource, info);
if (liveSource != null) {
streamSourceType = SourceType.LIVE_STREAM;
return liveSource;
}
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
final List<VideoStream> videos = ListHelper.getSortedStreamVideosList(context,
info.getVideoStreams(), info.getVideoOnlyStreams(), false, true);
videoStreams, videoOnlyStreams, false, true);
final int index;
if (videos.isEmpty()) {
index = -1;
@ -82,24 +93,34 @@ public class VideoPlaybackResolver implements PlaybackResolver {
.orElse(null);
if (video != null) {
final MediaSource streamSource = buildMediaSource(dataSource, video.getUrl(),
PlayerHelper.cacheKeyOf(info, video),
MediaFormat.getSuffixById(video.getFormatId()), tag);
mediaSources.add(streamSource);
try {
final MediaSource streamSource = PlaybackResolver.buildMediaSource(
dataSource, video, info, PlayerHelper.cacheKeyOf(info, video), tag);
mediaSources.add(streamSource);
} catch (final IOException e) {
Log.e(TAG, "Unable to create video source:", e);
return null;
}
}
// Create optional audio stream source
final List<AudioStream> audioStreams = info.getAudioStreams();
removeTorrentStreams(audioStreams);
final AudioStream audio = audioStreams.isEmpty() ? null : audioStreams.get(
ListHelper.getDefaultAudioFormat(context, audioStreams));
// Use the audio stream if there is no video stream, or
// Merge with audio stream in case if video does not contain audio
if (audio != null && (video == null || video.isVideoOnly)) {
final MediaSource audioSource = buildMediaSource(dataSource, audio.getUrl(),
PlayerHelper.cacheKeyOf(info, audio),
MediaFormat.getSuffixById(audio.getFormatId()), tag);
mediaSources.add(audioSource);
streamSourceType = SourceType.VIDEO_WITH_SEPARATED_AUDIO;
// merge with audio stream in case if video does not contain audio
if (audio != null && (video == null || video.isVideoOnly())) {
try {
final MediaSource audioSource = PlaybackResolver.buildMediaSource(
dataSource, audio, info, PlayerHelper.cacheKeyOf(info, audio), tag);
mediaSources.add(audioSource);
streamSourceType = SourceType.VIDEO_WITH_SEPARATED_AUDIO;
} catch (final IOException e) {
Log.e(TAG, "Unable to create audio source:", e);
return null;
}
} else {
streamSourceType = SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY;
}
@ -111,33 +132,35 @@ public class VideoPlaybackResolver implements PlaybackResolver {
// Below are auxiliary media sources
// Create subtitle sources
if (info.getSubtitles() != null) {
for (final SubtitlesStream subtitle : info.getSubtitles()) {
final String mimeType = PlayerHelper.subtitleMimeTypesOf(subtitle.getFormat());
if (mimeType == null) {
continue;
final List<SubtitlesStream> subtitlesStreams = info.getSubtitles();
if (subtitlesStreams != null) {
// Torrent and non URL subtitles are not supported by ExoPlayer
final List<SubtitlesStream> nonTorrentAndUrlStreams = removeNonUrlAndTorrentStreams(
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) {
return mediaSources.get(0);
} else {
return new MergingMediaSource(mediaSources.toArray(
new MediaSource[0]));
return new MergingMediaSource(true, mediaSources.toArray(new MediaSource[0]));
}
}

View File

@ -13,6 +13,8 @@ import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.DeliveryMethod;
import org.schabi.newpipe.extractor.stream.Stream;
import org.schabi.newpipe.extractor.stream.VideoStream;
import java.util.ArrayList;
@ -21,6 +23,7 @@ import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Set;
@ -37,10 +40,9 @@ public final class ListHelper {
// Audio format in order of efficiency. 0=most efficient, n=least efficient
private static final List<MediaFormat> AUDIO_FORMAT_EFFICIENCY_RANKING =
Arrays.asList(MediaFormat.WEBMA, MediaFormat.M4A, MediaFormat.MP3);
private static final Set<String> HIGH_RESOLUTION_LIST
// Uses a HashSet for better performance
= new HashSet<>(Arrays.asList("1440p", "2160p", "1440p60", "2160p60"));
// Use a HashSet for better performance
private static final Set<String> HIGH_RESOLUTION_LIST = new HashSet<>(
Arrays.asList("1440p", "2160p"));
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),
* 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,
final String bestResolutionKey,
final MediaFormat defaultFormat,
final List<VideoStream> videoStreams) {
@Nullable final List<VideoStream> videoStreams) {
if (videoStreams == null || videoStreams.isEmpty()) {
return -1;
}
@ -233,7 +312,9 @@ public final class ListHelper {
.flatMap(List::stream)
// Filter out higher resolutions (or not if high resolutions should always be shown)
.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());
final HashMap<String, VideoStream> hashMap = new HashMap<>();
@ -366,8 +447,9 @@ public final class ListHelper {
* @param videoStreams the available video streams
* @return the index of the preferred video stream
*/
static int getVideoStreamIndex(final String targetResolution, final MediaFormat targetFormat,
final List<VideoStream> videoStreams) {
static int getVideoStreamIndex(@NonNull final String targetResolution,
final MediaFormat targetFormat,
@NonNull final List<VideoStream> videoStreams) {
int fullMatchIndex = -1;
int fullMatchNoRefreshIndex = -1;
int resMatchOnlyIndex = -1;
@ -428,7 +510,7 @@ public final class ListHelper {
* @param videoStreams the list of video streams to check
* @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 List<VideoStream> videoStreams) {
final MediaFormat defaultFormat = getDefaultFormat(context,
@ -437,7 +519,7 @@ public final class ListHelper {
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 defaultFormatValueKey) {
final SharedPreferences preferences
@ -457,8 +539,8 @@ public final class ListHelper {
return defaultMediaFormat;
}
private static MediaFormat getMediaFormatFromKey(final Context context,
final String formatKey) {
private static MediaFormat getMediaFormatFromKey(@NonNull final Context context,
@NonNull final String formatKey) {
MediaFormat format = null;
if (formatKey.equals(context.getString(R.string.video_webm_key))) {
format = MediaFormat.WEBM;
@ -496,12 +578,20 @@ public final class ListHelper {
- formatRanking.indexOf(streamB.getFormat());
}
private static int compareVideoStreamResolution(final String r1, final String r2) {
final int res1 = Integer.parseInt(r1.replaceAll("0p\\d+$", "1")
.replaceAll("[^\\d.]", ""));
final int res2 = Integer.parseInt(r2.replaceAll("0p\\d+$", "1")
.replaceAll("[^\\d.]", ""));
return res1 - res2;
private static int compareVideoStreamResolution(@NonNull final String r1,
@NonNull final String r2) {
try {
final int res1 = Integer.parseInt(r1.replaceAll("0p\\d+$", "1")
.replaceAll("[^\\d.]", ""));
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.
@ -536,7 +626,7 @@ public final class ListHelper {
* @param context App context
* @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;
if (isMeteredNetwork(context)) {
final SharedPreferences preferences
@ -555,7 +645,7 @@ public final class ListHelper {
* @param context App context
* @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
= ContextCompat.getSystemService(context, ConnectivityManager.class);
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.exceptions.ExtractionException;
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.StreamInfoItem;
@ -60,7 +61,9 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.settings.SettingsActivity;
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 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,
@NonNull final StreamInfo info) {
final int index = ListHelper.getDefaultAudioFormat(context, info.getAudioStreams());
if (index == -1) {
final List<AudioStream> audioStreams = info.getAudioStreams();
if (audioStreams.isEmpty()) {
Toast.makeText(context, R.string.audio_streams_empty, Toast.LENGTH_SHORT).show();
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);
}
public static void playOnExternalVideoPlayer(@NonNull final Context context,
public static void playOnExternalVideoPlayer(final Context context,
@NonNull final StreamInfo info) {
final ArrayList<VideoStream> videoStreamsList = new ArrayList<>(
ListHelper.getSortedStreamVideosList(context, info.getVideoStreams(), null, false,
false));
final int index = ListHelper.getDefaultResolutionIndex(context, videoStreamsList);
if (index == -1) {
final List<VideoStream> videoStreams = info.getVideoStreams();
if (videoStreams.isEmpty()) {
Toast.makeText(context, R.string.video_streams_empty, Toast.LENGTH_SHORT).show();
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);
}
@ -248,9 +265,49 @@ public final class NavigationHelper {
@Nullable final String name,
@Nullable final String artist,
@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();
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("title", name);
intent.putExtra("artist", artist);

View File

@ -1,6 +1,7 @@
package org.schabi.newpipe.util;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.stream.AudioStream;
@ -14,7 +15,8 @@ public class SecondaryStreamHelper<T extends Stream> {
private final int position;
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.position = streams.getStreamsList().indexOf(selectedStream);
if (this.position < 0) {
@ -29,33 +31,37 @@ public class SecondaryStreamHelper<T extends Stream> {
* @param videoStream desired video ONLY stream
* @return selected audio stream or null if a candidate was not found
*/
@Nullable
public static AudioStream getAudioStreamFor(@NonNull final List<AudioStream> audioStreams,
@NonNull final VideoStream videoStream) {
switch (videoStream.getFormat()) {
case WEBM:
case MPEG_4:// ¿is mpeg-4 DASH?
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;
final MediaFormat mediaFormat = videoStream.getFormat();
if (mediaFormat != null) {
switch (mediaFormat) {
case WEBM:
case MPEG_4:// ¿is mpeg-4 DASH?
break;
default:
return null;
}
}
if (m4v) {
return null;
}
final boolean m4v = (mediaFormat == MediaFormat.MPEG_4);
// 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;
for (final AudioStream audio : audioStreams) {
if (audio.getFormat() == (m4v ? MediaFormat.M4A : MediaFormat.WEBMA)) {
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.TextView;
import androidx.annotation.NonNull;
import org.schabi.newpipe.DownloaderImpl;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.MediaFormat;
@ -87,7 +89,8 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
}
@Override
public View getDropDownView(final int position, final View convertView,
public View getDropDownView(final int position,
final View convertView,
final ViewGroup parent) {
return getCustomView(position, convertView, parent, true);
}
@ -98,7 +101,10 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
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) {
View convertView = view;
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 T stream = getItem(position);
final MediaFormat mediaFormat = stream.getFormat();
int woSoundIconVisibility = View.GONE;
String qualityString;
@ -135,24 +142,32 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
}
} else if (stream instanceof AudioStream) {
final AudioStream audioStream = ((AudioStream) stream);
qualityString = audioStream.getAverageBitrate() > 0
? audioStream.getAverageBitrate() + "kbps"
: audioStream.getFormat().getName();
if (audioStream.getAverageBitrate() > 0) {
qualityString = audioStream.getAverageBitrate() + "kbps";
} else if (mediaFormat != null) {
qualityString = mediaFormat.getName();
} else {
qualityString = context.getString(R.string.unknown_quality);
}
} else if (stream instanceof SubtitlesStream) {
qualityString = ((SubtitlesStream) stream).getDisplayLanguageName();
if (((SubtitlesStream) stream).isAutoGenerated()) {
qualityString += " (" + context.getString(R.string.caption_auto_generated) + ")";
}
} else {
qualityString = stream.getFormat().getSuffix();
if (mediaFormat != null) {
qualityString = mediaFormat.getSuffix();
} else {
qualityString = context.getString(R.string.unknown_quality);
}
}
if (streamsWrapper.getSizeInBytes(position) > 0) {
final SecondaryStreamHelper<U> secondary = secondaryStreams == null ? null
: secondaryStreams.get(position);
if (secondary != null) {
final long size
= secondary.getSizeInBytes() + streamsWrapper.getSizeInBytes(position);
final long size = secondary.getSizeInBytes()
+ streamsWrapper.getSizeInBytes(position);
sizeView.setText(Utility.formatBytes(size));
} else {
sizeView.setText(streamsWrapper.getFormattedSize(position));
@ -164,11 +179,15 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
if (stream instanceof SubtitlesStream) {
formatNameView.setText(((SubtitlesStream) stream).getLanguageTag());
} else if (stream.getFormat() == MediaFormat.WEBMA_OPUS) {
// noinspection AndroidLintSetTextI18n
formatNameView.setText("opus");
} 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);
@ -233,6 +252,7 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
* @param streamsWrapper the wrapper
* @return a {@link Single} that returns a boolean indicating if any elements were changed
*/
@NonNull
public static <X extends Stream> Single<Boolean> fetchSizeForWrapper(
final StreamSizeWrapper<X> streamsWrapper) {
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(
stream.getUrl());
stream.getContent());
streamsWrapper.setSize(stream, contentLength);
hasChanged = true;
}

View File

@ -3,7 +3,7 @@ package org.schabi.newpipe.util;
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 {
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
* @return <code>true</code> when the streamType is a
* @param streamType the stream type of the stream
* @return <code>true</code> if the streamType is a
* {@link StreamType#LIVE_STREAM} or {@link StreamType#AUDIO_LIVE_STREAM}
*/
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.exceptions.ExtractionException;
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.SubtitlesStream;
import org.schabi.newpipe.extractor.stream.VideoStream;
@ -131,31 +132,38 @@ public class DownloadMissionRecover extends Thread {
switch (mRecovery.getKind()) {
case 'a':
for (AudioStream audio : mExtractor.getAudioStreams()) {
if (audio.getAverageBitrate() == mRecovery.getDesiredBitrate() && audio.getFormat() == mRecovery.getFormat()) {
url = audio.getUrl();
for (final AudioStream audio : mExtractor.getAudioStreams()) {
if (audio.getAverageBitrate() == mRecovery.getDesiredBitrate()
&& audio.getFormat() == mRecovery.getFormat()
&& audio.getDeliveryMethod() == DeliveryMethod.PROGRESSIVE_HTTP) {
url = audio.getContent();
break;
}
}
break;
case 'v':
List<VideoStream> videoStreams;
final List<VideoStream> videoStreams;
if (mRecovery.isDesired2())
videoStreams = mExtractor.getVideoOnlyStreams();
else
videoStreams = mExtractor.getVideoStreams();
for (VideoStream video : videoStreams) {
if (video.resolution.equals(mRecovery.getDesired()) && video.getFormat() == mRecovery.getFormat()) {
url = video.getUrl();
for (final VideoStream video : videoStreams) {
if (video.getResolution().equals(mRecovery.getDesired())
&& video.getFormat() == mRecovery.getFormat()
&& video.getDeliveryMethod() == DeliveryMethod.PROGRESSIVE_HTTP) {
url = video.getContent();
break;
}
}
break;
case 's':
for (SubtitlesStream subtitles : mExtractor.getSubtitles(mRecovery.getFormat())) {
for (final SubtitlesStream subtitles : mExtractor.getSubtitles(mRecovery
.getFormat())) {
String tag = subtitles.getLanguageTag();
if (tag.equals(mRecovery.getDesired()) && subtitles.isAutoGenerated() == mRecovery.isDesired2()) {
url = subtitles.getUrl();
if (tag.equals(mRecovery.getDesired())
&& subtitles.isAutoGenerated() == mRecovery.isDesired2()
&& subtitles.getDeliveryMethod() == DeliveryMethod.PROGRESSIVE_HTTP) {
url = subtitles.getContent();
break;
}
}

View File

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

View File

@ -82,6 +82,7 @@
android:text="@string/msg_threads" />
<LinearLayout
android:id="@+id/threads_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/threads_text_view"
@ -106,4 +107,16 @@
android:max="31"
android:progress="3" />
</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>

View File

@ -740,4 +740,11 @@
<string name="you_successfully_subscribed">You now subscribed to this channel</string>
<string name="enumeration_comma">,</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>

View File

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