Merge pull request #8153 from AudricV/delivery-methods-v2

Support delivery methods other than progressive HTTP
This commit is contained in:
Stypox 2022-06-22 11:21:52 +02:00 committed by GitHub
commit 3901ffca17
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 2537 additions and 632 deletions

View File

@ -190,7 +190,7 @@ dependencies {
// name and the commit hash with the commit hash of the (pushed) commit you want to test // name and the commit hash with the commit hash of the (pushed) commit you want to test
// This works thanks to JitPack: https://jitpack.io/ // This works thanks to JitPack: https://jitpack.io/
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751' implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
implementation 'com.github.TeamNewPipe:NewPipeExtractor:ac1c22d81c65b7b0c5427f4e1989f5256d617f32' implementation 'com.github.TeamNewPipe:NewPipeExtractor:1b51eab664ec7cbd2295c96d8b43000379cd1b7b'
/** Checkstyle **/ /** Checkstyle **/
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}" checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"

View File

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

View File

@ -58,7 +58,6 @@ import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException;
import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException; import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo; import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.ktx.ExceptionUtils; import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.local.dialog.PlaylistDialog; import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.player.MainPlayer; import org.schabi.newpipe.player.MainPlayer;
@ -71,7 +70,6 @@ import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PermissionHelper; import org.schabi.newpipe.util.PermissionHelper;
@ -677,22 +675,13 @@ public class RouterActivity extends AppCompatActivity {
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> { .subscribe(result -> {
final List<VideoStream> sortedVideoStreams = ListHelper final DownloadDialog downloadDialog = new DownloadDialog(this, result);
.getSortedStreamVideosList(this, result.getVideoStreams(), downloadDialog.setOnDismissListener(dialog -> finish());
result.getVideoOnlyStreams(), false, false);
final int selectedVideoStreamIndex = ListHelper
.getDefaultResolutionIndex(this, sortedVideoStreams);
final FragmentManager fm = getSupportFragmentManager(); final FragmentManager fm = getSupportFragmentManager();
final DownloadDialog downloadDialog = DownloadDialog.newInstance(result);
downloadDialog.setVideoStreams(sortedVideoStreams);
downloadDialog.setAudioStreams(result.getAudioStreams());
downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex);
downloadDialog.setOnDismissListener(dialog -> finish());
downloadDialog.show(fm, "downloadDialog"); downloadDialog.show(fm, "downloadDialog");
fm.executePendingTransactions(); fm.executePendingTransactions();
}, throwable -> }, throwable -> showUnsupportedUrlDialog(currentUrl)));
showUnsupportedUrlDialog(currentUrl)));
} }
@Override @Override

View File

@ -12,8 +12,7 @@ import org.schabi.newpipe.database.BasicDAO
import org.schabi.newpipe.database.stream.model.StreamEntity import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_ID import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_ID
import org.schabi.newpipe.extractor.stream.StreamType import org.schabi.newpipe.extractor.stream.StreamType
import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_LIVE_STREAM import org.schabi.newpipe.util.StreamTypeUtil
import org.schabi.newpipe.extractor.stream.StreamType.LIVE_STREAM
import java.time.OffsetDateTime import java.time.OffsetDateTime
@Dao @Dao
@ -91,8 +90,7 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
?: throw IllegalStateException("Stream cannot be null just after insertion.") ?: throw IllegalStateException("Stream cannot be null just after insertion.")
newerStream.uid = existentMinimalStream.uid newerStream.uid = existentMinimalStream.uid
val isNewerStreamLive = newerStream.streamType == AUDIO_LIVE_STREAM || newerStream.streamType == LIVE_STREAM if (!StreamTypeUtil.isLiveStream(newerStream.streamType)) {
if (!isNewerStreamLive) {
// Use the existent upload date if the newer stream does not have a better precision // Use the existent upload date if the newer stream does not have a better precision
// (i.e. is an approximation). This is done to prevent unnecessary changes. // (i.e. is an approximation). This is done to prevent unnecessary changes.

View File

@ -68,9 +68,9 @@ import org.schabi.newpipe.util.ThemeHelper;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Objects;
import icepick.Icepick; import icepick.Icepick;
import icepick.State; import icepick.State;
@ -82,6 +82,8 @@ import us.shandian.giga.service.DownloadManagerService;
import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder; import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder;
import us.shandian.giga.service.MissionState; import us.shandian.giga.service.MissionState;
import static org.schabi.newpipe.extractor.stream.DeliveryMethod.PROGRESSIVE_HTTP;
import static org.schabi.newpipe.util.ListHelper.getStreamsOfSpecifiedDelivery;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
public class DownloadDialog extends DialogFragment public class DownloadDialog extends DialogFragment
@ -92,17 +94,17 @@ public class DownloadDialog extends DialogFragment
@State @State
StreamInfo currentInfo; StreamInfo currentInfo;
@State @State
StreamSizeWrapper<AudioStream> wrappedAudioStreams = StreamSizeWrapper.empty(); StreamSizeWrapper<AudioStream> wrappedAudioStreams;
@State @State
StreamSizeWrapper<VideoStream> wrappedVideoStreams = StreamSizeWrapper.empty(); StreamSizeWrapper<VideoStream> wrappedVideoStreams;
@State @State
StreamSizeWrapper<SubtitlesStream> wrappedSubtitleStreams = StreamSizeWrapper.empty(); StreamSizeWrapper<SubtitlesStream> wrappedSubtitleStreams;
@State @State
int selectedVideoIndex = 0; int selectedVideoIndex; // set in the constructor
@State @State
int selectedAudioIndex = 0; int selectedAudioIndex = 0; // default to the first item
@State @State
int selectedSubtitleIndex = 0; int selectedSubtitleIndex = 0; // default to the first item
@Nullable @Nullable
private OnDismissListener onDismissListener = null; private OnDismissListener onDismissListener = null;
@ -143,77 +145,43 @@ public class DownloadDialog extends DialogFragment
// Instance creation // Instance creation
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
public static DownloadDialog newInstance(final StreamInfo info) { /**
final DownloadDialog dialog = new DownloadDialog(); * Create a new download dialog with the video, audio and subtitle streams from the provided
dialog.setInfo(info); * stream info. Video streams and video-only streams will be put into a single list menu,
return dialog; * sorted according to their resolution and the default video resolution will be selected.
} *
* @param context the context to use just to obtain preferences and strings (will not be stored)
public static DownloadDialog newInstance(final Context context, final StreamInfo info) { * @param info the info from which to obtain downloadable streams and other info (e.g. title)
final ArrayList<VideoStream> streamsList = new ArrayList<>(ListHelper */
.getSortedStreamVideosList(context, info.getVideoStreams(), public DownloadDialog(final Context context, @NonNull final StreamInfo info) {
info.getVideoOnlyStreams(), false, false));
final int selectedStreamIndex = ListHelper.getDefaultResolutionIndex(context, streamsList);
final DownloadDialog instance = newInstance(info);
instance.setVideoStreams(streamsList);
instance.setSelectedVideoStream(selectedStreamIndex);
instance.setAudioStreams(info.getAudioStreams());
instance.setSubtitleStreams(info.getSubtitles());
return instance;
}
/*//////////////////////////////////////////////////////////////////////////
// Setters
//////////////////////////////////////////////////////////////////////////*/
private void setInfo(final StreamInfo info) {
this.currentInfo = info; this.currentInfo = info;
// TODO: Adapt this code when the downloader support other types of stream deliveries
final List<VideoStream> videoStreams = ListHelper.getSortedStreamVideosList(
context,
getStreamsOfSpecifiedDelivery(info.getVideoStreams(), PROGRESSIVE_HTTP),
getStreamsOfSpecifiedDelivery(info.getVideoOnlyStreams(), PROGRESSIVE_HTTP),
false,
false
);
this.wrappedVideoStreams = new StreamSizeWrapper<>(videoStreams, context);
this.wrappedAudioStreams = new StreamSizeWrapper<>(
getStreamsOfSpecifiedDelivery(info.getAudioStreams(), PROGRESSIVE_HTTP), context);
this.wrappedSubtitleStreams = new StreamSizeWrapper<>(
getStreamsOfSpecifiedDelivery(info.getSubtitles(), PROGRESSIVE_HTTP), context);
this.selectedVideoIndex = ListHelper.getDefaultResolutionIndex(context, videoStreams);
} }
public void setAudioStreams(final List<AudioStream> audioStreams) { /**
setAudioStreams(new StreamSizeWrapper<>(audioStreams, getContext())); * @param onDismissListener the listener to call in {@link #onDismiss(DialogInterface)}
} */
public void setAudioStreams(final StreamSizeWrapper<AudioStream> was) {
this.wrappedAudioStreams = was;
}
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 setSelectedVideoStream(final int svi) {
this.selectedVideoIndex = svi;
}
public void setSelectedAudioStream(final int sai) {
this.selectedAudioIndex = sai;
}
public void setSelectedSubtitleStream(final int ssi) {
this.selectedSubtitleIndex = ssi;
}
public void setOnDismissListener(@Nullable final OnDismissListener onDismissListener) { public void setOnDismissListener(@Nullable final OnDismissListener onDismissListener) {
this.onDismissListener = onDismissListener; this.onDismissListener = onDismissListener;
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Android lifecycle // Android lifecycle
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -249,11 +217,16 @@ public class DownloadDialog extends DialogFragment
.getAudioStreamFor(wrappedAudioStreams.getStreamsList(), videoStreams.get(i)); .getAudioStreamFor(wrappedAudioStreams.getStreamsList(), videoStreams.get(i));
if (audioStream != null) { if (audioStream != null) {
secondaryStreams secondaryStreams.append(i, new SecondaryStreamHelper<>(wrappedAudioStreams,
.append(i, new SecondaryStreamHelper<>(wrappedAudioStreams, audioStream)); audioStream));
} else if (DEBUG) { } else if (DEBUG) {
Log.w(TAG, "No audio stream candidates for video format " final MediaFormat mediaFormat = videoStreams.get(i).getFormat();
+ videoStreams.get(i).getFormat().name()); if (mediaFormat != null) {
Log.w(TAG, "No audio stream candidates for video format "
+ mediaFormat.name());
} else {
Log.w(TAG, "No audio stream candidates for unknown video format");
}
} }
} }
@ -288,7 +261,8 @@ public class DownloadDialog extends DialogFragment
} }
@Override @Override
public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container, public View onCreateView(@NonNull final LayoutInflater inflater,
final ViewGroup container,
final Bundle savedInstanceState) { final Bundle savedInstanceState) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "onCreateView() called with: " Log.d(TAG, "onCreateView() called with: "
@ -299,14 +273,15 @@ public class DownloadDialog extends DialogFragment
} }
@Override @Override
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { public void onViewCreated(@NonNull final View view,
@Nullable final Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
dialogBinding = DownloadDialogBinding.bind(view); dialogBinding = DownloadDialogBinding.bind(view);
dialogBinding.fileName.setText(FilenameUtils.createFilename(getContext(), dialogBinding.fileName.setText(FilenameUtils.createFilename(getContext(),
currentInfo.getName())); currentInfo.getName()));
selectedAudioIndex = ListHelper selectedAudioIndex = ListHelper
.getDefaultAudioFormat(getContext(), currentInfo.getAudioStreams()); .getDefaultAudioFormat(getContext(), wrappedAudioStreams.getStreamsList());
selectedSubtitleIndex = getSubtitleIndexBy(subtitleStreamsAdapter.getAll()); selectedSubtitleIndex = getSubtitleIndexBy(subtitleStreamsAdapter.getAll());
@ -324,7 +299,8 @@ public class DownloadDialog extends DialogFragment
dialogBinding.threads.setProgress(threads - 1); dialogBinding.threads.setProgress(threads - 1);
dialogBinding.threads.setOnSeekBarChangeListener(new SimpleOnSeekBarChangeListener() { dialogBinding.threads.setOnSeekBarChangeListener(new SimpleOnSeekBarChangeListener() {
@Override @Override
public void onProgressChanged(@NonNull final SeekBar seekbar, final int progress, public void onProgressChanged(@NonNull final SeekBar seekbar,
final int progress,
final boolean fromUser) { final boolean fromUser) {
final int newProgress = progress + 1; final int newProgress = progress + 1;
prefs.edit().putInt(getString(R.string.default_download_threads), newProgress) prefs.edit().putInt(getString(R.string.default_download_threads), newProgress)
@ -469,7 +445,7 @@ public class DownloadDialog extends DialogFragment
result, getString(R.string.download_path_video_key), DownloadManager.TAG_VIDEO); result, getString(R.string.download_path_video_key), DownloadManager.TAG_VIDEO);
} }
private void requestDownloadSaveAsResult(final ActivityResult result) { private void requestDownloadSaveAsResult(@NonNull final ActivityResult result) {
if (result.getResultCode() != Activity.RESULT_OK) { if (result.getResultCode() != Activity.RESULT_OK) {
return; return;
} }
@ -486,8 +462,8 @@ public class DownloadDialog extends DialogFragment
return; return;
} }
final DocumentFile docFile final DocumentFile docFile = DocumentFile.fromSingleUri(context,
= DocumentFile.fromSingleUri(context, result.getData().getData()); result.getData().getData());
if (docFile == null) { if (docFile == null) {
showFailedDialog(R.string.general_error); showFailedDialog(R.string.general_error);
return; return;
@ -498,7 +474,7 @@ public class DownloadDialog extends DialogFragment
docFile.getType()); docFile.getType());
} }
private void requestDownloadPickFolderResult(final ActivityResult result, private void requestDownloadPickFolderResult(@NonNull final ActivityResult result,
final String key, final String key,
final String tag) { final String tag) {
if (result.getResultCode() != Activity.RESULT_OK) { if (result.getResultCode() != Activity.RESULT_OK) {
@ -518,12 +494,11 @@ public class DownloadDialog extends DialogFragment
StoredDirectoryHelper.PERMISSION_FLAGS); StoredDirectoryHelper.PERMISSION_FLAGS);
} }
PreferenceManager.getDefaultSharedPreferences(context).edit() PreferenceManager.getDefaultSharedPreferences(context).edit().putString(key,
.putString(key, uri.toString()).apply(); uri.toString()).apply();
try { try {
final StoredDirectoryHelper mainStorage final StoredDirectoryHelper mainStorage = new StoredDirectoryHelper(context, uri, tag);
= new StoredDirectoryHelper(context, uri, tag);
checkSelectedDownload(mainStorage, mainStorage.findFile(filenameTmp), checkSelectedDownload(mainStorage, mainStorage.findFile(filenameTmp),
filenameTmp, mimeTmp); filenameTmp, mimeTmp);
} catch (final IOException e) { } catch (final IOException e) {
@ -561,8 +536,10 @@ public class DownloadDialog extends DialogFragment
} }
@Override @Override
public void onItemSelected(final AdapterView<?> parent, final View view, public void onItemSelected(final AdapterView<?> parent,
final int position, final long id) { final View view,
final int position,
final long id) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "onItemSelected() called with: " Log.d(TAG, "onItemSelected() called with: "
+ "parent = [" + parent + "], view = [" + view + "], " + "parent = [" + parent + "], view = [" + view + "], "
@ -597,14 +574,16 @@ public class DownloadDialog extends DialogFragment
final boolean isAudioStreamsAvailable = audioStreamsAdapter.getCount() > 0; final boolean isAudioStreamsAvailable = audioStreamsAdapter.getCount() > 0;
final boolean isSubtitleStreamsAvailable = subtitleStreamsAdapter.getCount() > 0; final boolean isSubtitleStreamsAvailable = subtitleStreamsAdapter.getCount() > 0;
dialogBinding.audioButton.setVisibility(isAudioStreamsAvailable ? View.VISIBLE : View.GONE); dialogBinding.audioButton.setVisibility(isAudioStreamsAvailable ? View.VISIBLE
dialogBinding.videoButton.setVisibility(isVideoStreamsAvailable ? View.VISIBLE : View.GONE); : View.GONE);
dialogBinding.videoButton.setVisibility(isVideoStreamsAvailable ? View.VISIBLE
: View.GONE);
dialogBinding.subtitleButton.setVisibility(isSubtitleStreamsAvailable dialogBinding.subtitleButton.setVisibility(isSubtitleStreamsAvailable
? View.VISIBLE : View.GONE); ? View.VISIBLE : View.GONE);
prefs = PreferenceManager.getDefaultSharedPreferences(requireContext()); prefs = PreferenceManager.getDefaultSharedPreferences(requireContext());
final String defaultMedia = prefs.getString(getString(R.string.last_used_download_type), final String defaultMedia = prefs.getString(getString(R.string.last_used_download_type),
getString(R.string.last_download_type_video_key)); getString(R.string.last_download_type_video_key));
if (isVideoStreamsAvailable if (isVideoStreamsAvailable
&& (defaultMedia.equals(getString(R.string.last_download_type_video_key)))) { && (defaultMedia.equals(getString(R.string.last_download_type_video_key)))) {
@ -640,7 +619,7 @@ public class DownloadDialog extends DialogFragment
dialogBinding.subtitleButton.setEnabled(enabled); dialogBinding.subtitleButton.setEnabled(enabled);
} }
private int getSubtitleIndexBy(final List<SubtitlesStream> streams) { private int getSubtitleIndexBy(@NonNull final List<SubtitlesStream> streams) {
final Localization preferredLocalization = NewPipe.getPreferredLocalization(); final Localization preferredLocalization = NewPipe.getPreferredLocalization();
int candidate = 0; int candidate = 0;
@ -666,8 +645,10 @@ public class DownloadDialog extends DialogFragment
return candidate; return candidate;
} }
@NonNull
private String getNameEditText() { private String getNameEditText() {
final String str = dialogBinding.fileName.getText().toString().trim(); final String str = Objects.requireNonNull(dialogBinding.fileName.getText()).toString()
.trim();
return FilenameUtils.createFilename(context, str.isEmpty() ? currentInfo.getName() : str); return FilenameUtils.createFilename(context, str.isEmpty() ? currentInfo.getName() : str);
} }
@ -683,12 +664,8 @@ public class DownloadDialog extends DialogFragment
} }
private void launchDirectoryPicker(final ActivityResultLauncher<Intent> launcher) { private void launchDirectoryPicker(final ActivityResultLauncher<Intent> launcher) {
NoFileManagerSafeGuard.launchSafe( NoFileManagerSafeGuard.launchSafe(launcher, StoredDirectoryHelper.getPicker(context), TAG,
launcher, context);
StoredDirectoryHelper.getPicker(context),
TAG,
context
);
} }
private void prepareSelectedDownload() { private void prepareSelectedDownload() {
@ -709,7 +686,7 @@ public class DownloadDialog extends DialogFragment
if (format == MediaFormat.WEBMA_OPUS) { if (format == MediaFormat.WEBMA_OPUS) {
mimeTmp = "audio/ogg"; mimeTmp = "audio/ogg";
filenameTmp += "opus"; filenameTmp += "opus";
} else { } else if (format != null) {
mimeTmp = format.mimeType; mimeTmp = format.mimeType;
filenameTmp += format.suffix; filenameTmp += format.suffix;
} }
@ -718,22 +695,30 @@ public class DownloadDialog extends DialogFragment
selectedMediaType = getString(R.string.last_download_type_video_key); selectedMediaType = getString(R.string.last_download_type_video_key);
mainStorage = mainStorageVideo; mainStorage = mainStorageVideo;
format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat(); format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
mimeTmp = format.mimeType; if (format != null) {
filenameTmp += format.suffix; mimeTmp = format.mimeType;
filenameTmp += format.suffix;
}
break; break;
case R.id.subtitle_button: case R.id.subtitle_button:
selectedMediaType = getString(R.string.last_download_type_subtitle_key); selectedMediaType = getString(R.string.last_download_type_subtitle_key);
mainStorage = mainStorageVideo; // subtitle & video files go together mainStorage = mainStorageVideo; // subtitle & video files go together
format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat(); format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat();
mimeTmp = format.mimeType; if (format != null) {
filenameTmp += (format == MediaFormat.TTML ? MediaFormat.SRT : format).suffix; mimeTmp = format.mimeType;
}
if (format == MediaFormat.TTML) {
filenameTmp += MediaFormat.SRT.suffix;
} else if (format != null) {
filenameTmp += format.suffix;
}
break; break;
default: default:
throw new RuntimeException("No stream selected"); throw new RuntimeException("No stream selected");
} }
if (!askForSavePath if (!askForSavePath && (mainStorage == null
&& (mainStorage == null
|| mainStorage.isDirect() == NewPipeSettings.useStorageAccessFramework(context) || mainStorage.isDirect() == NewPipeSettings.useStorageAccessFramework(context)
|| mainStorage.isInvalidSafStorage())) { || mainStorage.isInvalidSafStorage())) {
// Pick new download folder if one of: // Pick new download folder if one of:
@ -767,18 +752,16 @@ public class DownloadDialog extends DialogFragment
initialPath = Uri.parse(initialSavePath.getAbsolutePath()); initialPath = Uri.parse(initialSavePath.getAbsolutePath());
} }
NoFileManagerSafeGuard.launchSafe( NoFileManagerSafeGuard.launchSafe(requestDownloadSaveAsLauncher,
requestDownloadSaveAsLauncher, StoredFileHelper.getNewPicker(context, filenameTmp, mimeTmp, initialPath), TAG,
StoredFileHelper.getNewPicker(context, filenameTmp, mimeTmp, initialPath), context);
TAG,
context
);
return; return;
} }
// check for existing file with the same name // check for existing file with the same name
checkSelectedDownload(mainStorage, mainStorage.findFile(filenameTmp), filenameTmp, mimeTmp); checkSelectedDownload(mainStorage, mainStorage.findFile(filenameTmp), filenameTmp,
mimeTmp);
// remember the last media type downloaded by the user // remember the last media type downloaded by the user
prefs.edit().putString(getString(R.string.last_used_download_type), selectedMediaType) prefs.edit().putString(getString(R.string.last_used_download_type), selectedMediaType)
@ -786,7 +769,8 @@ public class DownloadDialog extends DialogFragment
} }
private void checkSelectedDownload(final StoredDirectoryHelper mainStorage, private void checkSelectedDownload(final StoredDirectoryHelper mainStorage,
final Uri targetFile, final String filename, final Uri targetFile,
final String filename,
final String mime) { final String mime) {
StoredFileHelper storage; StoredFileHelper storage;
@ -947,7 +931,7 @@ public class DownloadDialog extends DialogFragment
storage.truncate(); storage.truncate();
} }
} catch (final IOException e) { } catch (final IOException e) {
Log.e(TAG, "failed to truncate the file: " + storage.getUri().toString(), e); Log.e(TAG, "Failed to truncate the file: " + storage.getUri().toString(), e);
showFailedDialog(R.string.overwrite_failed); showFailedDialog(R.string.overwrite_failed);
return; return;
} }
@ -992,8 +976,8 @@ public class DownloadDialog extends DialogFragment
} }
psArgs = null; psArgs = null;
final long videoSize = wrappedVideoStreams final long videoSize = wrappedVideoStreams.getSizeInBytes(
.getSizeInBytes((VideoStream) selectedStream); (VideoStream) selectedStream);
// set nearLength, only, if both sizes are fetched or known. This probably // set nearLength, only, if both sizes are fetched or known. This probably
// does not work on slow networks but is later updated in the downloader // does not work on slow networks but is later updated in the downloader
@ -1009,7 +993,7 @@ public class DownloadDialog extends DialogFragment
if (selectedStream.getFormat() == MediaFormat.TTML) { if (selectedStream.getFormat() == MediaFormat.TTML) {
psName = Postprocessing.ALGORITHM_TTML_CONVERTER; psName = Postprocessing.ALGORITHM_TTML_CONVERTER;
psArgs = new String[]{ psArgs = new String[] {
selectedStream.getFormat().getSuffix(), selectedStream.getFormat().getSuffix(),
"false" // ignore empty frames "false" // ignore empty frames
}; };
@ -1020,17 +1004,22 @@ public class DownloadDialog extends DialogFragment
} }
if (secondaryStream == null) { if (secondaryStream == null) {
urls = new String[]{ urls = new String[] {
selectedStream.getUrl() selectedStream.getContent()
}; };
recoveryInfo = new MissionRecoveryInfo[]{ recoveryInfo = new MissionRecoveryInfo[] {
new MissionRecoveryInfo(selectedStream) new MissionRecoveryInfo(selectedStream)
}; };
} else { } else {
urls = new String[]{ if (secondaryStream.getDeliveryMethod() != PROGRESSIVE_HTTP) {
selectedStream.getUrl(), secondaryStream.getUrl() throw new IllegalArgumentException("Unsupported stream delivery format"
+ secondaryStream.getDeliveryMethod());
}
urls = new String[] {
selectedStream.getContent(), secondaryStream.getContent()
}; };
recoveryInfo = new MissionRecoveryInfo[]{new MissionRecoveryInfo(selectedStream), recoveryInfo = new MissionRecoveryInfo[] {new MissionRecoveryInfo(selectedStream),
new MissionRecoveryInfo(secondaryStream)}; new MissionRecoveryInfo(secondaryStream)};
} }

View File

@ -31,6 +31,7 @@ import android.view.WindowManager;
import android.view.animation.DecelerateInterpolator; import android.view.animation.DecelerateInterpolator;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.widget.RelativeLayout; import android.widget.RelativeLayout;
import android.widget.Toast;
import androidx.annotation.AttrRes; import androidx.annotation.AttrRes;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -94,6 +95,7 @@ import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PermissionHelper; import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.PicassoHelper; import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.StreamTypeUtil;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.util.external_communication.KoreUtils; import org.schabi.newpipe.util.external_communication.KoreUtils;
import org.schabi.newpipe.util.external_communication.ShareUtils; import org.schabi.newpipe.util.external_communication.ShareUtils;
@ -121,6 +123,7 @@ import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientat
import static org.schabi.newpipe.player.helper.PlayerHelper.isClearingQueueConfirmationRequired; import static org.schabi.newpipe.player.helper.PlayerHelper.isClearingQueueConfirmationRequired;
import static org.schabi.newpipe.player.playqueue.PlayQueueItem.RECOVERY_UNSET; import static org.schabi.newpipe.player.playqueue.PlayQueueItem.RECOVERY_UNSET;
import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView; import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
import static org.schabi.newpipe.util.ListHelper.getUrlAndNonTorrentStreams;
public final class VideoDetailFragment public final class VideoDetailFragment
extends BaseStateFragment<StreamInfo> extends BaseStateFragment<StreamInfo>
@ -186,8 +189,6 @@ public final class VideoDetailFragment
@Nullable @Nullable
private Disposable positionSubscriber = null; private Disposable positionSubscriber = null;
private List<VideoStream> sortedVideoStreams;
private int selectedVideoStreamIndex = -1;
private BottomSheetBehavior<FrameLayout> bottomSheetBehavior; private BottomSheetBehavior<FrameLayout> bottomSheetBehavior;
private BroadcastReceiver broadcastReceiver; private BroadcastReceiver broadcastReceiver;
@ -1092,9 +1093,6 @@ public final class VideoDetailFragment
} }
private void openBackgroundPlayer(final boolean append) { private void openBackgroundPlayer(final boolean append) {
final AudioStream audioStream = currentInfo.getAudioStreams()
.get(ListHelper.getDefaultAudioFormat(activity, currentInfo.getAudioStreams()));
final boolean useExternalAudioPlayer = PreferenceManager final boolean useExternalAudioPlayer = PreferenceManager
.getDefaultSharedPreferences(activity) .getDefaultSharedPreferences(activity)
.getBoolean(activity.getString(R.string.use_external_audio_player_key), false); .getBoolean(activity.getString(R.string.use_external_audio_player_key), false);
@ -1109,7 +1107,17 @@ public final class VideoDetailFragment
if (!useExternalAudioPlayer) { if (!useExternalAudioPlayer) {
openNormalBackgroundPlayer(append); openNormalBackgroundPlayer(append);
} else { } else {
startOnExternalPlayer(activity, currentInfo, audioStream); final List<AudioStream> audioStreams = getUrlAndNonTorrentStreams(
currentInfo.getAudioStreams());
final int index = ListHelper.getDefaultAudioFormat(activity, audioStreams);
if (index == -1) {
Toast.makeText(activity, R.string.no_audio_streams_available_for_external_players,
Toast.LENGTH_SHORT).show();
return;
}
startOnExternalPlayer(activity, currentInfo, audioStreams.get(index));
} }
} }
@ -1612,14 +1620,6 @@ public final class VideoDetailFragment
binding.detailToggleSecondaryControlsView.setVisibility(View.VISIBLE); binding.detailToggleSecondaryControlsView.setVisibility(View.VISIBLE);
binding.detailSecondaryControlPanel.setVisibility(View.GONE); binding.detailSecondaryControlPanel.setVisibility(View.GONE);
sortedVideoStreams = ListHelper.getSortedStreamVideosList(
activity,
info.getVideoStreams(),
info.getVideoOnlyStreams(),
false,
false);
selectedVideoStreamIndex = ListHelper
.getDefaultResolutionIndex(activity, sortedVideoStreams);
updateProgressInfo(info); updateProgressInfo(info);
initThumbnailViews(info); initThumbnailViews(info);
showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView, showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView,
@ -1645,8 +1645,8 @@ public final class VideoDetailFragment
} }
} }
binding.detailControlsDownload.setVisibility(info.getStreamType() == StreamType.LIVE_STREAM binding.detailControlsDownload.setVisibility(
|| info.getStreamType() == StreamType.AUDIO_LIVE_STREAM ? View.GONE : View.VISIBLE); StreamTypeUtil.isLiveStream(info.getStreamType()) ? View.GONE : View.VISIBLE);
binding.detailControlsBackground.setVisibility(info.getAudioStreams().isEmpty() binding.detailControlsBackground.setVisibility(info.getAudioStreams().isEmpty()
? View.GONE : View.VISIBLE); ? View.GONE : View.VISIBLE);
@ -1687,12 +1687,7 @@ public final class VideoDetailFragment
} }
try { try {
final DownloadDialog downloadDialog = DownloadDialog.newInstance(currentInfo); final DownloadDialog downloadDialog = new DownloadDialog(activity, currentInfo);
downloadDialog.setVideoStreams(sortedVideoStreams);
downloadDialog.setAudioStreams(currentInfo.getAudioStreams());
downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex);
downloadDialog.setSubtitleStreams(currentInfo.getSubtitles());
downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog"); downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
} catch (final Exception e) { } catch (final Exception e) {
ErrorUtil.showSnackbar(activity, new ErrorInfo(e, UserAction.DOWNLOAD_OPEN_DIALOG, ErrorUtil.showSnackbar(activity, new ErrorInfo(e, UserAction.DOWNLOAD_OPEN_DIALOG,
@ -1722,8 +1717,7 @@ public final class VideoDetailFragment
binding.detailPositionView.setVisibility(View.GONE); binding.detailPositionView.setVisibility(View.GONE);
// TODO: Remove this check when separation of concerns is done. // TODO: Remove this check when separation of concerns is done.
// (live streams weren't getting updated because they are mixed) // (live streams weren't getting updated because they are mixed)
if (!info.getStreamType().equals(StreamType.LIVE_STREAM) if (!StreamTypeUtil.isLiveStream(info.getStreamType())) {
&& !info.getStreamType().equals(StreamType.AUDIO_LIVE_STREAM)) {
return; return;
} }
} else { } else {
@ -2151,25 +2145,52 @@ public final class VideoDetailFragment
} }
private void showExternalPlaybackDialog() { private void showExternalPlaybackDialog() {
if (sortedVideoStreams == null) { if (currentInfo == null) {
return; return;
} }
final CharSequence[] resolutions = new CharSequence[sortedVideoStreams.size()];
for (int i = 0; i < sortedVideoStreams.size(); i++) { final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
resolutions[i] = sortedVideoStreams.get(i).getResolution(); builder.setTitle(R.string.select_quality_external_players);
} builder.setNeutralButton(R.string.open_in_browser, (dialog, i) ->
final AlertDialog.Builder builder = new AlertDialog.Builder(activity) ShareUtils.openUrlInBrowser(requireActivity(), url));
.setNegativeButton(R.string.cancel, null)
.setNeutralButton(R.string.open_in_browser, (dialog, i) -> final List<VideoStream> videoStreamsForExternalPlayers =
ShareUtils.openUrlInBrowser(requireActivity(), url) ListHelper.getSortedStreamVideosList(
activity,
getUrlAndNonTorrentStreams(currentInfo.getVideoStreams()),
getUrlAndNonTorrentStreams(currentInfo.getVideoOnlyStreams()),
false,
false
); );
// Maybe there are no video streams available, show just `open in browser` button
if (resolutions.length > 0) { if (videoStreamsForExternalPlayers.isEmpty()) {
builder.setSingleChoiceItems(resolutions, selectedVideoStreamIndex, (dialog, i) -> { builder.setMessage(R.string.no_video_streams_available_for_external_players);
dialog.dismiss(); builder.setPositiveButton(R.string.ok, null);
startOnExternalPlayer(activity, currentInfo, sortedVideoStreams.get(i));
} } 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,
null);
builder.setNegativeButton(R.string.cancel, null);
builder.setPositiveButton(R.string.ok, (dialog, i) -> {
final int index = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
// We don't have to manage the index validity because if there is no stream
// available for external players, this code will be not executed and if there is
// no stream which matches the default resolution, 0 is returned by
// ListHelper.getDefaultResolutionIndex.
// The index cannot be outside the bounds of the list as its always between 0 and
// the list size - 1, .
startOnExternalPlayer(activity, currentInfo,
videoStreamsForExternalPlayers.get(index));
});
} }
builder.show(); builder.show();
} }

View File

@ -24,6 +24,7 @@ import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.player.helper.PlayerHolder; import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.util.StreamTypeUtil;
import org.schabi.newpipe.util.external_communication.KoreUtils; import org.schabi.newpipe.util.external_communication.KoreUtils;
import java.util.ArrayList; import java.util.ArrayList;
@ -269,8 +270,7 @@ public final class InfoItemDialog {
*/ */
public Builder addStartHereEntries() { public Builder addStartHereEntries() {
addEntry(StreamDialogDefaultEntry.START_HERE_ON_BACKGROUND); addEntry(StreamDialogDefaultEntry.START_HERE_ON_BACKGROUND);
if (infoItem.getStreamType() != StreamType.AUDIO_STREAM if (!StreamTypeUtil.isAudio(infoItem.getStreamType())) {
&& infoItem.getStreamType() != StreamType.AUDIO_LIVE_STREAM) {
addEntry(StreamDialogDefaultEntry.START_HERE_ON_POPUP); addEntry(StreamDialogDefaultEntry.START_HERE_ON_POPUP);
} }
return this; return this;
@ -285,9 +285,7 @@ public final class InfoItemDialog {
final boolean isWatchHistoryEnabled = PreferenceManager final boolean isWatchHistoryEnabled = PreferenceManager
.getDefaultSharedPreferences(context) .getDefaultSharedPreferences(context)
.getBoolean(context.getString(R.string.enable_watch_history_key), false); .getBoolean(context.getString(R.string.enable_watch_history_key), false);
if (isWatchHistoryEnabled if (isWatchHistoryEnabled && !StreamTypeUtil.isLiveStream(infoItem.getStreamType())) {
&& infoItem.getStreamType() != StreamType.LIVE_STREAM
&& infoItem.getStreamType() != StreamType.AUDIO_LIVE_STREAM) {
addEntry(StreamDialogDefaultEntry.MARK_AS_WATCHED); addEntry(StreamDialogDefaultEntry.MARK_AS_WATCHED);
} }
return this; return this;

View File

@ -11,12 +11,12 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.database.stream.model.StreamStateEntity;
import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.ktx.ViewUtils; import org.schabi.newpipe.ktx.ViewUtils;
import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.PicassoHelper; import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.StreamTypeUtil;
import org.schabi.newpipe.views.AnimatedProgressBar; import org.schabi.newpipe.views.AnimatedProgressBar;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -70,8 +70,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
} else { } else {
itemProgressView.setVisibility(View.GONE); itemProgressView.setVisibility(View.GONE);
} }
} else if (item.getStreamType() == StreamType.LIVE_STREAM } else if (StreamTypeUtil.isLiveStream(item.getStreamType())) {
|| item.getStreamType() == StreamType.AUDIO_LIVE_STREAM) {
itemDurationView.setText(R.string.duration_live); itemDurationView.setText(R.string.duration_live);
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(), itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(),
R.color.live_duration_background_color)); R.color.live_duration_background_color));
@ -96,9 +95,10 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
case VIDEO_STREAM: case VIDEO_STREAM:
case LIVE_STREAM: case LIVE_STREAM:
case AUDIO_LIVE_STREAM: case AUDIO_LIVE_STREAM:
case POST_LIVE_STREAM:
case POST_LIVE_AUDIO_STREAM:
enableLongClick(item); enableLongClick(item);
break; break;
case FILE:
case NONE: case NONE:
default: default:
disableLongClick(); disableLongClick();
@ -114,7 +114,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
final StreamStateEntity state final StreamStateEntity state
= historyRecordManager.loadStreamState(infoItem).blockingGet()[0]; = historyRecordManager.loadStreamState(infoItem).blockingGet()[0];
if (state != null && item.getDuration() > 0 if (state != null && item.getDuration() > 0
&& item.getStreamType() != StreamType.LIVE_STREAM) { && !StreamTypeUtil.isLiveStream(item.getStreamType())) {
itemProgressView.setMax((int) item.getDuration()); itemProgressView.setMax((int) item.getDuration());
if (itemProgressView.getVisibility() == View.VISIBLE) { if (itemProgressView.getVisibility() == View.VISIBLE) {
itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS

View File

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

View File

@ -150,7 +150,6 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.squareup.picasso.Picasso; import com.squareup.picasso.Picasso;
import com.squareup.picasso.Target; import com.squareup.picasso.Target;
import org.schabi.newpipe.DownloaderImpl;
import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.database.stream.model.StreamEntity; import org.schabi.newpipe.database.stream.model.StreamEntity;
@ -429,7 +428,7 @@ public final class Player implements
setupBroadcastReceiver(); setupBroadcastReceiver();
trackSelector = new DefaultTrackSelector(context, PlayerHelper.getQualitySelector()); trackSelector = new DefaultTrackSelector(context, PlayerHelper.getQualitySelector());
final PlayerDataSource dataSource = new PlayerDataSource(context, DownloaderImpl.USER_AGENT, final PlayerDataSource dataSource = new PlayerDataSource(context,
new DefaultBandwidthMeter.Builder(context).build()); new DefaultBandwidthMeter.Builder(context).build());
loadController = new LoadController(); loadController = new LoadController();
renderFactory = new DefaultRenderersFactory(context); renderFactory = new DefaultRenderersFactory(context);
@ -1744,24 +1743,9 @@ public final class Player implements
if (exoPlayerIsNull()) { if (exoPlayerIsNull()) {
return; return;
} }
// Use duration of currentItem for non-live streams,
// because HLS streams are fragmented onUpdateProgress(Math.max((int) simpleExoPlayer.getCurrentPosition(), 0),
// and thus the whole duration is not available to the player (int) simpleExoPlayer.getDuration(), simpleExoPlayer.getBufferedPercentage());
// TODO: revert #6307 when introducing proper HLS support
final int duration;
if (currentItem != null
&& !StreamTypeUtil.isLiveStream(currentItem.getStreamType())
) {
// convert seconds to milliseconds
duration = (int) (currentItem.getDuration() * 1000);
} else {
duration = (int) simpleExoPlayer.getDuration();
}
onUpdateProgress(
Math.max((int) simpleExoPlayer.getCurrentPosition(), 0),
duration,
simpleExoPlayer.getBufferedPercentage()
);
} }
private Disposable getProgressUpdateDisposable() { private Disposable getProgressUpdateDisposable() {
@ -3399,6 +3383,7 @@ public final class Player implements
switch (info.getStreamType()) { switch (info.getStreamType()) {
case AUDIO_STREAM: case AUDIO_STREAM:
case POST_LIVE_AUDIO_STREAM:
binding.surfaceView.setVisibility(View.GONE); binding.surfaceView.setVisibility(View.GONE);
binding.endScreen.setVisibility(View.VISIBLE); binding.endScreen.setVisibility(View.VISIBLE);
binding.playbackEndTime.setVisibility(View.VISIBLE); binding.playbackEndTime.setVisibility(View.VISIBLE);
@ -3417,6 +3402,7 @@ public final class Player implements
break; break;
case VIDEO_STREAM: case VIDEO_STREAM:
case POST_LIVE_STREAM:
if (currentMetadata == null if (currentMetadata == null
|| !currentMetadata.getMaybeQuality().isPresent() || !currentMetadata.getMaybeQuality().isPresent()
|| (info.getVideoStreams().isEmpty() || (info.getVideoStreams().isEmpty()
@ -3484,10 +3470,10 @@ public final class Player implements
for (int i = 0; i < availableStreams.size(); i++) { for (int i = 0; i < availableStreams.size(); i++) {
final VideoStream videoStream = availableStreams.get(i); final VideoStream videoStream = availableStreams.get(i);
qualityPopupMenu.getMenu().add(POPUP_MENU_ID_QUALITY, i, Menu.NONE, MediaFormat qualityPopupMenu.getMenu().add(POPUP_MENU_ID_QUALITY, i, Menu.NONE, MediaFormat
.getNameById(videoStream.getFormatId()) + " " + videoStream.resolution); .getNameById(videoStream.getFormatId()) + " " + videoStream.getResolution());
} }
if (getSelectedVideoStream() != null) { if (getSelectedVideoStream() != null) {
binding.qualityTextView.setText(getSelectedVideoStream().resolution); binding.qualityTextView.setText(getSelectedVideoStream().getResolution());
} }
qualityPopupMenu.setOnMenuItemClickListener(this); qualityPopupMenu.setOnMenuItemClickListener(this);
qualityPopupMenu.setOnDismissListener(this); qualityPopupMenu.setOnDismissListener(this);
@ -3605,7 +3591,7 @@ public final class Player implements
} }
saveStreamProgressState(); //TODO added, check if good saveStreamProgressState(); //TODO added, check if good
final String newResolution = availableStreams.get(menuItemIndex).resolution; final String newResolution = availableStreams.get(menuItemIndex).getResolution();
setRecovery(); setRecovery();
setPlaybackQuality(newResolution); setPlaybackQuality(newResolution);
reloadPlayQueueManager(); reloadPlayQueueManager();
@ -3633,7 +3619,7 @@ public final class Player implements
} }
isSomePopupMenuVisible = false; //TODO check if this works isSomePopupMenuVisible = false; //TODO check if this works
if (getSelectedVideoStream() != null) { if (getSelectedVideoStream() != null) {
binding.qualityTextView.setText(getSelectedVideoStream().resolution); binding.qualityTextView.setText(getSelectedVideoStream().getResolution());
} }
if (isPlaying()) { if (isPlaying()) {
hideControls(DEFAULT_CONTROLS_DURATION, 0); hideControls(DEFAULT_CONTROLS_DURATION, 0);
@ -4248,9 +4234,7 @@ public final class Player implements
if (playQueueManagerReloadingNeeded(sourceType, info, getVideoRendererIndex())) { if (playQueueManagerReloadingNeeded(sourceType, info, getVideoRendererIndex())) {
reloadPlayQueueManager(); reloadPlayQueueManager();
} else { } else {
final StreamType streamType = info.getStreamType(); if (StreamTypeUtil.isAudio(info.getStreamType())) {
if (streamType == StreamType.AUDIO_STREAM
|| streamType == StreamType.AUDIO_LIVE_STREAM) {
// Nothing to do more than setting the recovery position // Nothing to do more than setting the recovery position
setRecovery(); setRecovery();
return; return;
@ -4285,13 +4269,15 @@ public final class Player implements
* the content is not an audio content, but also if none of the following cases is met: * the content is not an audio content, but also if none of the following cases is met:
* *
* <ul> * <ul>
* <li>the content is an {@link StreamType#AUDIO_STREAM audio stream} or an * <li>the content is an {@link StreamType#AUDIO_STREAM audio stream}, an
* {@link StreamType#AUDIO_LIVE_STREAM audio live stream};</li> * {@link StreamType#AUDIO_LIVE_STREAM audio live stream}, or a
* {@link StreamType#POST_LIVE_AUDIO_STREAM ended audio live stream};</li>
* <li>the content is a {@link StreamType#LIVE_STREAM live stream} and the source type is a * <li>the content is a {@link StreamType#LIVE_STREAM live stream} and the source type is a
* {@link SourceType#LIVE_STREAM live source};</li> * {@link SourceType#LIVE_STREAM live source};</li>
* <li>the content's source is {@link SourceType#VIDEO_WITH_SEPARATED_AUDIO a video stream * <li>the content's source is {@link SourceType#VIDEO_WITH_SEPARATED_AUDIO a video stream
* with a separated audio source} or has no audio-only streams available <b>and</b> is a * with a separated audio source} or has no audio-only streams available <b>and</b> is a
* {@link StreamType#LIVE_STREAM live stream} or a * {@link StreamType#VIDEO_STREAM video stream}, an
* {@link StreamType#POST_LIVE_STREAM ended live stream}, or a
* {@link StreamType#LIVE_STREAM live stream}. * {@link StreamType#LIVE_STREAM live stream}.
* </li> * </li>
* </ul> * </ul>
@ -4307,18 +4293,17 @@ public final class Player implements
@NonNull final StreamInfo streamInfo, @NonNull final StreamInfo streamInfo,
final int videoRendererIndex) { final int videoRendererIndex) {
final StreamType streamType = streamInfo.getStreamType(); final StreamType streamType = streamInfo.getStreamType();
final boolean isStreamTypeAudio = StreamTypeUtil.isAudio(streamType);
if (videoRendererIndex == RENDERER_UNAVAILABLE && streamType != StreamType.AUDIO_STREAM if (videoRendererIndex == RENDERER_UNAVAILABLE && !isStreamTypeAudio) {
&& streamType != StreamType.AUDIO_LIVE_STREAM) {
return true; return true;
} }
// The content is an audio stream, an audio live stream, or a live stream with a live // The content is an audio stream, an audio live stream, or a live stream with a live
// source: it's not needed to reload the play queue manager because the stream source will // source: it's not needed to reload the play queue manager because the stream source will
// be the same // be the same
if ((streamType == StreamType.AUDIO_STREAM || streamType == StreamType.AUDIO_LIVE_STREAM) if (isStreamTypeAudio || (streamType == StreamType.LIVE_STREAM
|| (streamType == StreamType.LIVE_STREAM && sourceType == SourceType.LIVE_STREAM)) {
&& sourceType == SourceType.LIVE_STREAM)) {
return false; return false;
} }
@ -4331,8 +4316,8 @@ public final class Player implements
|| (sourceType == SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY || (sourceType == SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY
&& isNullOrEmpty(streamInfo.getAudioStreams()))) { && isNullOrEmpty(streamInfo.getAudioStreams()))) {
// It's not needed to reload the play queue manager only if the content's stream type // It's not needed to reload the play queue manager only if the content's stream type
// is a video stream or a live stream // is a video stream, a live stream or an ended live stream
return streamType != StreamType.VIDEO_STREAM && streamType != StreamType.LIVE_STREAM; return !StreamTypeUtil.isVideo(streamType);
} }
// Other cases: the play queue manager reload is needed // Other cases: the play queue manager reload is needed

View File

@ -0,0 +1,136 @@
package org.schabi.newpipe.player.datasource;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
import androidx.annotation.NonNull;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.source.hls.HlsDataSourceFactory;
import com.google.android.exoplayer2.upstream.ByteArrayDataSource;
import com.google.android.exoplayer2.upstream.DataSource;
import java.nio.charset.StandardCharsets;
/**
* A {@link HlsDataSourceFactory} which allows playback of non-URI media HLS playlists for
* {@link com.google.android.exoplayer2.source.hls.HlsMediaSource HlsMediaSource}s.
*
* <p>
* If media requests are relative, the URI from which the manifest comes from (either the
* manifest URI (preferred) or the master URI (if applicable)) must be returned, otherwise the
* content will be not playable, as it will be an invalid URL, or it may be treat as something
* unexpected, for instance as a file for
* {@link com.google.android.exoplayer2.upstream.DefaultDataSource DefaultDataSource}s.
* </p>
*
* <p>
* See {@link #createDataSource(int)} for changes and implementation details.
* </p>
*/
public final class NonUriHlsDataSourceFactory implements HlsDataSourceFactory {
/**
* Builder class of {@link NonUriHlsDataSourceFactory} instances.
*/
public static final class Builder {
private DataSource.Factory dataSourceFactory;
private String playlistString;
/**
* Set the {@link DataSource.Factory} which will be used to create non manifest contents
* {@link DataSource}s.
*
* @param dataSourceFactoryForNonManifestContents the {@link DataSource.Factory} which will
* be used to create non manifest contents
* {@link DataSource}s, which cannot be null
*/
public void setDataSourceFactory(
@NonNull final DataSource.Factory dataSourceFactoryForNonManifestContents) {
this.dataSourceFactory = dataSourceFactoryForNonManifestContents;
}
/**
* Set the HLS playlist which will be used for manifests requests.
*
* @param hlsPlaylistString the string which correspond to the response of the HLS
* manifest, which cannot be null or empty
*/
public void setPlaylistString(@NonNull final String hlsPlaylistString) {
this.playlistString = hlsPlaylistString;
}
/**
* Create a new {@link NonUriHlsDataSourceFactory} with the given data source factory and
* the given HLS playlist.
*
* @return a {@link NonUriHlsDataSourceFactory}
* @throws IllegalArgumentException if the data source factory is null or if the HLS
* playlist string set is null or empty
*/
@NonNull
public NonUriHlsDataSourceFactory build() {
if (dataSourceFactory == null) {
throw new IllegalArgumentException(
"No DataSource.Factory valid instance has been specified.");
}
if (isNullOrEmpty(playlistString)) {
throw new IllegalArgumentException("No HLS valid playlist has been specified.");
}
return new NonUriHlsDataSourceFactory(dataSourceFactory,
playlistString.getBytes(StandardCharsets.UTF_8));
}
}
private final DataSource.Factory dataSourceFactory;
private final byte[] playlistStringByteArray;
/**
* Create a {@link NonUriHlsDataSourceFactory} instance.
*
* @param dataSourceFactory the {@link DataSource.Factory} which will be used to build
* non manifests {@link DataSource}s, which must not be null
* @param playlistStringByteArray a byte array of the HLS playlist, which must not be null
*/
private NonUriHlsDataSourceFactory(@NonNull final DataSource.Factory dataSourceFactory,
@NonNull final byte[] playlistStringByteArray) {
this.dataSourceFactory = dataSourceFactory;
this.playlistStringByteArray = playlistStringByteArray;
}
/**
* Create a {@link DataSource} for the given data type.
*
* <p>
* Contrary to {@link com.google.android.exoplayer2.source.hls.DefaultHlsDataSourceFactory
* ExoPlayer's default implementation}, this implementation is not always using the
* {@link DataSource.Factory} passed to the
* {@link com.google.android.exoplayer2.source.hls.HlsMediaSource.Factory
* HlsMediaSource.Factory} constructor, only when it's not
* {@link C#DATA_TYPE_MANIFEST the manifest type}.
* </p>
*
* <p>
* This change allow playback of non-URI HLS contents, when the manifest is not a master
* manifest/playlist (otherwise, endless loops should be encountered because the
* {@link DataSource}s created for media playlists should use the master playlist response
* instead).
* </p>
*
* @param dataType the data type for which the {@link DataSource} will be used, which is one of
* {@link C} {@code .DATA_TYPE_*} constants
* @return a {@link DataSource} for the given data type
*/
@NonNull
@Override
public DataSource createDataSource(final int dataType) {
// The manifest is already downloaded and provided with playlistStringByteArray, so we
// don't need to download it again and we can use a ByteArrayDataSource instead
if (dataType == C.DATA_TYPE_MANIFEST) {
return new ByteArrayDataSource(playlistStringByteArray);
}
return dataSourceFactory.createDataSource();
}
}

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,7 +1,13 @@
package org.schabi.newpipe.player.helper; package org.schabi.newpipe.player.helper;
import android.content.Context; import static org.schabi.newpipe.MainActivity.DEBUG;
import android.content.Context;
import android.util.Log;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.database.StandaloneDatabaseProvider;
import com.google.android.exoplayer2.source.ProgressiveMediaSource; import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.source.SingleSampleMediaSource; import com.google.android.exoplayer2.source.SingleSampleMediaSource;
import com.google.android.exoplayer2.source.dash.DashMediaSource; import com.google.android.exoplayer2.source.dash.DashMediaSource;
@ -13,12 +19,21 @@ import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSource; import com.google.android.exoplayer2.upstream.DefaultDataSource;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor;
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
import androidx.annotation.NonNull; import org.schabi.newpipe.DownloaderImpl;
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.NonUriHlsDataSourceFactory;
import org.schabi.newpipe.player.datasource.YoutubeHttpDataSource;
import java.io.File;
public class PlayerDataSource { public class PlayerDataSource {
public static final String TAG = PlayerDataSource.class.getSimpleName();
public static final int LIVE_STREAM_EDGE_GAP_MILLIS = 10000; public static final int LIVE_STREAM_EDGE_GAP_MILLIS = 10000;
@ -29,79 +44,174 @@ public class PlayerDataSource {
* early. * early.
*/ */
private static final double PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT = 15; private static final double PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT = 15;
private static final int MANIFEST_MINIMUM_RETRY = 5;
private static final int EXTRACTOR_MINIMUM_RETRY = Integer.MAX_VALUE;
private final int continueLoadingCheckIntervalBytes; /**
private final DataSource.Factory cacheDataSourceFactory; * The maximum number of generated manifests per cache, in
* {@link YoutubeProgressiveDashManifestCreator}, {@link YoutubeOtfDashManifestCreator} and
* {@link YoutubePostLiveStreamDvrDashManifestCreator}.
*/
private static final int MAX_MANIFEST_CACHE_SIZE = 500;
/**
* The folder name in which the ExoPlayer cache will be written.
*/
private static final String CACHE_FOLDER_NAME = "exoplayer";
/**
* The {@link SimpleCache} instance which will be used to build
* {@link com.google.android.exoplayer2.upstream.cache.CacheDataSource}s instances (with
* {@link CacheFactory}).
*/
private static SimpleCache cache;
private final int progressiveLoadIntervalBytes;
// Generic Data Source Factories (without or with cache)
private final DataSource.Factory cachelessDataSourceFactory; private final DataSource.Factory cachelessDataSourceFactory;
private final CacheFactory cacheDataSourceFactory;
public PlayerDataSource(@NonNull final Context context, // YouTube-specific Data Source Factories (with cache)
@NonNull final String userAgent, // They use YoutubeHttpDataSource.Factory, with different parameters each
@NonNull final TransferListener transferListener) { private final CacheFactory ytHlsCacheDataSourceFactory;
continueLoadingCheckIntervalBytes = PlayerHelper.getProgressiveLoadIntervalBytes(context); private final CacheFactory ytDashCacheDataSourceFactory;
cacheDataSourceFactory = new CacheFactory(context, userAgent, transferListener); private final CacheFactory ytProgressiveDashCacheDataSourceFactory;
cachelessDataSourceFactory = new DefaultDataSource
.Factory(context, new DefaultHttpDataSource.Factory().setUserAgent(userAgent))
public PlayerDataSource(final Context context,
final TransferListener transferListener) {
progressiveLoadIntervalBytes = PlayerHelper.getProgressiveLoadIntervalBytes(context);
// make sure the static cache was created: needed by CacheFactories below
instantiateCacheIfNeeded(context);
// generic data source factories use DefaultHttpDataSource.Factory
cachelessDataSourceFactory = new DefaultDataSource.Factory(context,
new DefaultHttpDataSource.Factory().setUserAgent(DownloaderImpl.USER_AGENT))
.setTransferListener(transferListener); .setTransferListener(transferListener);
cacheDataSourceFactory = new CacheFactory(context, transferListener, cache,
new DefaultHttpDataSource.Factory().setUserAgent(DownloaderImpl.USER_AGENT));
// YouTube-specific data source factories use getYoutubeHttpDataSourceFactory()
ytHlsCacheDataSourceFactory = new CacheFactory(context, transferListener, cache,
getYoutubeHttpDataSourceFactory(false, false));
ytDashCacheDataSourceFactory = new CacheFactory(context, transferListener, cache,
getYoutubeHttpDataSourceFactory(true, true));
ytProgressiveDashCacheDataSourceFactory = new CacheFactory(context, transferListener, cache,
getYoutubeHttpDataSourceFactory(false, true));
// set the maximum size to manifest creators
YoutubeProgressiveDashManifestCreator.getCache().setMaximumSize(MAX_MANIFEST_CACHE_SIZE);
YoutubeOtfDashManifestCreator.getCache().setMaximumSize(MAX_MANIFEST_CACHE_SIZE);
YoutubePostLiveStreamDvrDashManifestCreator.getCache().setMaximumSize(
MAX_MANIFEST_CACHE_SIZE);
} }
//region Live media source factories
public SsMediaSource.Factory getLiveSsMediaSourceFactory() { public SsMediaSource.Factory getLiveSsMediaSourceFactory() {
return new SsMediaSource.Factory( return getSSMediaSourceFactory().setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS);
new DefaultSsChunkSource.Factory(cachelessDataSourceFactory),
cachelessDataSourceFactory
)
.setLoadErrorHandlingPolicy(
new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY))
.setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS);
} }
public HlsMediaSource.Factory getLiveHlsMediaSourceFactory() { public HlsMediaSource.Factory getLiveHlsMediaSourceFactory() {
return new HlsMediaSource.Factory(cachelessDataSourceFactory) return new HlsMediaSource.Factory(cachelessDataSourceFactory)
.setAllowChunklessPreparation(true) .setAllowChunklessPreparation(true)
.setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(
MANIFEST_MINIMUM_RETRY))
.setPlaylistTrackerFactory((dataSourceFactory, loadErrorHandlingPolicy, .setPlaylistTrackerFactory((dataSourceFactory, loadErrorHandlingPolicy,
playlistParserFactory) -> playlistParserFactory) ->
new DefaultHlsPlaylistTracker(dataSourceFactory, loadErrorHandlingPolicy, new DefaultHlsPlaylistTracker(dataSourceFactory, loadErrorHandlingPolicy,
playlistParserFactory, PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT) playlistParserFactory,
); PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT));
} }
public DashMediaSource.Factory getLiveDashMediaSourceFactory() { public DashMediaSource.Factory getLiveDashMediaSourceFactory() {
return new DashMediaSource.Factory( return new DashMediaSource.Factory(
getDefaultDashChunkSourceFactory(cachelessDataSourceFactory), getDefaultDashChunkSourceFactory(cachelessDataSourceFactory),
cachelessDataSourceFactory cachelessDataSourceFactory);
)
.setLoadErrorHandlingPolicy(
new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY));
} }
//endregion
private DefaultDashChunkSource.Factory getDefaultDashChunkSourceFactory(
final DataSource.Factory dataSourceFactory
) {
return new DefaultDashChunkSource.Factory(dataSourceFactory);
}
public HlsMediaSource.Factory getHlsMediaSourceFactory() { //region Generic media source factories
public HlsMediaSource.Factory getHlsMediaSourceFactory(
@Nullable final NonUriHlsDataSourceFactory.Builder hlsDataSourceFactoryBuilder) {
if (hlsDataSourceFactoryBuilder != null) {
hlsDataSourceFactoryBuilder.setDataSourceFactory(cacheDataSourceFactory);
return new HlsMediaSource.Factory(hlsDataSourceFactoryBuilder.build());
}
return new HlsMediaSource.Factory(cacheDataSourceFactory); return new HlsMediaSource.Factory(cacheDataSourceFactory);
} }
public DashMediaSource.Factory getDashMediaSourceFactory() { public DashMediaSource.Factory getDashMediaSourceFactory() {
return new DashMediaSource.Factory( return new DashMediaSource.Factory(
getDefaultDashChunkSourceFactory(cacheDataSourceFactory), getDefaultDashChunkSourceFactory(cacheDataSourceFactory),
cacheDataSourceFactory cacheDataSourceFactory);
);
} }
public ProgressiveMediaSource.Factory getExtractorMediaSourceFactory() { public ProgressiveMediaSource.Factory getProgressiveMediaSourceFactory() {
return new ProgressiveMediaSource.Factory(cacheDataSourceFactory) return new ProgressiveMediaSource.Factory(cacheDataSourceFactory)
.setContinueLoadingCheckIntervalBytes(continueLoadingCheckIntervalBytes) .setContinueLoadingCheckIntervalBytes(progressiveLoadIntervalBytes);
.setLoadErrorHandlingPolicy(
new DefaultLoadErrorHandlingPolicy(EXTRACTOR_MINIMUM_RETRY));
} }
public SingleSampleMediaSource.Factory getSampleMediaSourceFactory() { public SsMediaSource.Factory getSSMediaSourceFactory() {
return new SsMediaSource.Factory(
new DefaultSsChunkSource.Factory(cachelessDataSourceFactory),
cachelessDataSourceFactory);
}
public SingleSampleMediaSource.Factory getSingleSampleMediaSourceFactory() {
return new SingleSampleMediaSource.Factory(cacheDataSourceFactory); return new SingleSampleMediaSource.Factory(cacheDataSourceFactory);
} }
//endregion
//region YouTube media source factories
public HlsMediaSource.Factory getYoutubeHlsMediaSourceFactory() {
return new HlsMediaSource.Factory(ytHlsCacheDataSourceFactory);
}
public DashMediaSource.Factory getYoutubeDashMediaSourceFactory() {
return new DashMediaSource.Factory(
getDefaultDashChunkSourceFactory(ytDashCacheDataSourceFactory),
ytDashCacheDataSourceFactory);
}
public ProgressiveMediaSource.Factory getYoutubeProgressiveMediaSourceFactory() {
return new ProgressiveMediaSource.Factory(ytProgressiveDashCacheDataSourceFactory)
.setContinueLoadingCheckIntervalBytes(progressiveLoadIntervalBytes);
}
//endregion
//region Static methods
private static DefaultDashChunkSource.Factory getDefaultDashChunkSourceFactory(
final DataSource.Factory dataSourceFactory) {
return new DefaultDashChunkSource.Factory(dataSourceFactory);
}
private static YoutubeHttpDataSource.Factory getYoutubeHttpDataSourceFactory(
final boolean rangeParameterEnabled,
final boolean rnParameterEnabled) {
return new YoutubeHttpDataSource.Factory()
.setRangeParameterEnabled(rangeParameterEnabled)
.setRnParameterEnabled(rnParameterEnabled);
}
private static void instantiateCacheIfNeeded(final Context context) {
if (cache == null) {
final File cacheDir = new File(context.getExternalCacheDir(), CACHE_FOLDER_NAME);
if (DEBUG) {
Log.d(TAG, "instantiateCacheIfNeeded: cacheDir = " + cacheDir.getAbsolutePath());
}
if (!cacheDir.exists() && !cacheDir.mkdir()) {
Log.w(TAG, "instantiateCacheIfNeeded: could not create cache dir");
}
final LeastRecentlyUsedCacheEvictor evictor
= new LeastRecentlyUsedCacheEvictor(PlayerHelper.getPreferredCacheSize());
cache = new SimpleCache(cacheDir, evictor, new StandaloneDatabaseProvider(context));
}
}
//endregion
} }

View File

@ -45,11 +45,9 @@ import com.google.android.exoplayer2.util.MimeTypes;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.SubtitlesStream; import org.schabi.newpipe.extractor.stream.SubtitlesStream;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.extractor.utils.Utils; import org.schabi.newpipe.extractor.utils.Utils;
import org.schabi.newpipe.player.MainPlayer; import org.schabi.newpipe.player.MainPlayer;
import org.schabi.newpipe.player.Player; import org.schabi.newpipe.player.Player;
@ -110,12 +108,14 @@ public final class PlayerHelper {
int MINIMIZE_ON_EXIT_MODE_POPUP = 2; int MINIMIZE_ON_EXIT_MODE_POPUP = 2;
} }
private PlayerHelper() { } private PlayerHelper() {
}
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// Exposed helpers // Exposed helpers
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
@NonNull
public static String getTimeString(final int milliSeconds) { public static String getTimeString(final int milliSeconds) {
final int seconds = (milliSeconds % 60000) / 1000; final int seconds = (milliSeconds % 60000) / 1000;
final int minutes = (milliSeconds % 3600000) / 60000; final int minutes = (milliSeconds % 3600000) / 60000;
@ -131,15 +131,18 @@ public final class PlayerHelper {
).toString(); ).toString();
} }
@NonNull
public static String formatSpeed(final double speed) { public static String formatSpeed(final double speed) {
return SPEED_FORMATTER.format(speed); return SPEED_FORMATTER.format(speed);
} }
@NonNull
public static String formatPitch(final double pitch) { public static String formatPitch(final double pitch) {
return PITCH_FORMATTER.format(pitch); return PITCH_FORMATTER.format(pitch);
} }
public static String subtitleMimeTypesOf(final MediaFormat format) { @NonNull
public static String subtitleMimeTypesOf(@NonNull final MediaFormat format) {
switch (format) { switch (format) {
case VTT: case VTT:
return MimeTypes.TEXT_VTT; return MimeTypes.TEXT_VTT;
@ -190,18 +193,6 @@ 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
public static String cacheKeyOf(@NonNull final StreamInfo info,
@NonNull final AudioStream audio) {
return info.getUrl() + audio.getAverageBitrate() + audio.getFormat().getName();
}
/** /**
* Given a {@link StreamInfo} and the existing queue items, * Given a {@link StreamInfo} and the existing queue items,
* provide the {@link SinglePlayQueue} consisting of the next video for auto queueing. * provide the {@link SinglePlayQueue} consisting of the next video for auto queueing.
@ -233,7 +224,7 @@ public final class PlayerHelper {
return null; return null;
} }
if (relatedItems.get(0) != null && relatedItems.get(0) instanceof StreamInfoItem if (relatedItems.get(0) instanceof StreamInfoItem
&& !urls.contains(relatedItems.get(0).getUrl())) { && !urls.contains(relatedItems.get(0).getUrl())) {
return getAutoQueuedSinglePlayQueue((StreamInfoItem) relatedItems.get(0)); return getAutoQueuedSinglePlayQueue((StreamInfoItem) relatedItems.get(0));
} }
@ -335,6 +326,7 @@ public final class PlayerHelper {
return 2 * 1024 * 1024L; // ExoPlayer CacheDataSink.MIN_RECOMMENDED_FRAGMENT_SIZE return 2 * 1024 * 1024L; // ExoPlayer CacheDataSink.MIN_RECOMMENDED_FRAGMENT_SIZE
} }
@NonNull
public static ExoTrackSelection.Factory getQualitySelector() { public static ExoTrackSelection.Factory getQualitySelector() {
return new AdaptiveTrackSelection.Factory( return new AdaptiveTrackSelection.Factory(
1000, 1000,
@ -389,7 +381,7 @@ public final class PlayerHelper {
/** /**
* @param context the Android context * @param context the Android context
* @return the screen brightness to use. A value less than 0 (the default) means to use the * @return the screen brightness to use. A value less than 0 (the default) means to use the
* preferred screen brightness * preferred screen brightness
*/ */
public static float getScreenBrightness(@NonNull final Context context) { public static float getScreenBrightness(@NonNull final Context context) {
final SharedPreferences sp = getPreferences(context); final SharedPreferences sp = getPreferences(context);
@ -480,7 +472,8 @@ public final class PlayerHelper {
return REPEAT_MODE_ONE; return REPEAT_MODE_ONE;
case REPEAT_MODE_ONE: case REPEAT_MODE_ONE:
return REPEAT_MODE_ALL; return REPEAT_MODE_ALL;
case REPEAT_MODE_ALL: default: case REPEAT_MODE_ALL:
default:
return REPEAT_MODE_OFF; return REPEAT_MODE_OFF;
} }
} }
@ -548,7 +541,7 @@ public final class PlayerHelper {
player.getContext().getResources().getDimension(R.dimen.popup_default_width); player.getContext().getResources().getDimension(R.dimen.popup_default_width);
final float popupWidth = popupRememberSizeAndPos final float popupWidth = popupRememberSizeAndPos
? player.getPrefs().getFloat(player.getContext().getString( ? player.getPrefs().getFloat(player.getContext().getString(
R.string.popup_saved_width_key), defaultSize) R.string.popup_saved_width_key), defaultSize)
: defaultSize; : defaultSize;
final float popupHeight = getMinimumVideoHeight(popupWidth); final float popupHeight = getMinimumVideoHeight(popupWidth);
@ -564,10 +557,10 @@ public final class PlayerHelper {
final int centerY = (int) (player.getScreenHeight() / 2f - popupHeight / 2f); final int centerY = (int) (player.getScreenHeight() / 2f - popupHeight / 2f);
popupLayoutParams.x = popupRememberSizeAndPos popupLayoutParams.x = popupRememberSizeAndPos
? player.getPrefs().getInt(player.getContext().getString( ? player.getPrefs().getInt(player.getContext().getString(
R.string.popup_saved_x_key), centerX) : centerX; R.string.popup_saved_x_key), centerX) : centerX;
popupLayoutParams.y = popupRememberSizeAndPos popupLayoutParams.y = popupRememberSizeAndPos
? player.getPrefs().getInt(player.getContext().getString( ? player.getPrefs().getInt(player.getContext().getString(
R.string.popup_saved_y_key), centerY) : centerY; R.string.popup_saved_y_key), centerY) : centerY;
return popupLayoutParams; return popupLayoutParams;
} }

View File

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

View File

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

View File

@ -1,50 +1,193 @@
package org.schabi.newpipe.player.resolver; package org.schabi.newpipe.player.resolver;
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.helper.PlayerDataSource.LIVE_STREAM_EDGE_GAP_MILLIS;
import android.net.Uri; import android.net.Uri;
import android.text.TextUtils; import android.util.Log;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.source.dash.DashMediaSource;
import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser;
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
import com.google.android.exoplayer2.source.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.MediaFormat;
import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.extractor.services.youtube.ItagItem;
import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.CreationException;
import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeOtfDashManifestCreator;
import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubePostLiveStreamDvrDashManifestCreator;
import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeProgressiveDashManifestCreator;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.DeliveryMethod;
import org.schabi.newpipe.extractor.stream.Stream;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.player.datasource.NonUriHlsDataSourceFactory;
import org.schabi.newpipe.player.helper.PlayerDataSource; import org.schabi.newpipe.player.helper.PlayerDataSource;
import org.schabi.newpipe.player.mediaitem.MediaItemTag; import org.schabi.newpipe.player.mediaitem.MediaItemTag;
import org.schabi.newpipe.player.mediaitem.StreamInfoTag; import org.schabi.newpipe.player.mediaitem.StreamInfoTag;
import org.schabi.newpipe.util.StreamTypeUtil; import org.schabi.newpipe.util.StreamTypeUtil;
import androidx.annotation.NonNull; import java.io.ByteArrayInputStream;
import androidx.annotation.Nullable; import java.io.IOException;
import java.nio.charset.StandardCharsets;
import static org.schabi.newpipe.player.helper.PlayerDataSource.LIVE_STREAM_EDGE_GAP_MILLIS; import java.util.Objects;
/**
* This interface is just a shorthand for {@link Resolver} with {@link StreamInfo} as source and
* {@link MediaSource} as product. It contains many static methods that can be used by classes
* implementing this interface, and nothing else.
*/
public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> { public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
String TAG = PlaybackResolver.class.getSimpleName();
//region Cache key generation
private static StringBuilder commonCacheKeyOf(final StreamInfo info,
final Stream stream,
final boolean resolutionOrBitrateUnknown) {
// stream info service id
final StringBuilder cacheKey = new StringBuilder(info.getServiceId());
// stream info id
cacheKey.append(" ");
cacheKey.append(info.getId());
// stream id (even if unknown)
cacheKey.append(" ");
cacheKey.append(stream.getId());
// mediaFormat (if not null)
final MediaFormat mediaFormat = stream.getFormat();
if (mediaFormat != null) {
cacheKey.append(" ");
cacheKey.append(mediaFormat.getName());
}
// content (only if other information is missing)
// If the media format and the resolution/bitrate are both missing, then we don't have
// enough information to distinguish this stream from other streams.
// So, only in that case, we use the content (i.e. url or manifest) to differentiate
// between streams.
// Note that if the content were used even when other information is present, then two
// streams with the same stats but with different contents (e.g. because the url was
// refreshed) will be considered different (i.e. with a different cacheKey), making the
// cache useless.
if (resolutionOrBitrateUnknown && mediaFormat == null) {
cacheKey.append(" ");
cacheKey.append(Objects.hash(stream.getContent(), stream.getManifestUrl()));
}
return cacheKey;
}
/**
* Builds the cache key of a {@link VideoStream video stream}.
*
* <p>
* A cache key is unique to the features of the provided video stream, and when possible
* independent of <i>transient</i> parameters (such as the URL of the stream).
* This ensures that there are no conflicts, but also that the cache is used as much as
* possible: the same cache should be used for two streams which have the same features but
* e.g. a different URL, since the URL might have been reloaded in the meantime, but the stream
* actually referenced by the URL is still the same.
* </p>
*
* @param info the {@link StreamInfo stream info}, to distinguish between streams with
* the same features but coming from different stream infos
* @param videoStream the {@link VideoStream video stream} for which the cache key should be
* created
* @return a key to be used to store the cache of the provided {@link VideoStream video stream}
*/
static String cacheKeyOf(final StreamInfo info, final VideoStream videoStream) {
final boolean resolutionUnknown = videoStream.getResolution().equals(RESOLUTION_UNKNOWN);
final StringBuilder cacheKey = commonCacheKeyOf(info, videoStream, resolutionUnknown);
// resolution (if known)
if (!resolutionUnknown) {
cacheKey.append(" ");
cacheKey.append(videoStream.getResolution());
}
// isVideoOnly
cacheKey.append(" ");
cacheKey.append(videoStream.isVideoOnly());
return cacheKey.toString();
}
/**
* Builds the cache key of an audio stream.
*
* <p>
* A cache key is unique to the features of the provided {@link AudioStream audio stream}, and
* when possible independent of <i>transient</i> parameters (such as the URL of the stream).
* This ensures that there are no conflicts, but also that the cache is used as much as
* possible: the same cache should be used for two streams which have the same features but
* e.g. a different URL, since the URL might have been reloaded in the meantime, but the stream
* actually referenced by the URL is still the same.
* </p>
*
* @param info the {@link StreamInfo stream info}, to distinguish between streams with
* the same features but coming from different stream infos
* @param audioStream the {@link AudioStream audio stream} for which the cache key should be
* created
* @return a key to be used to store the cache of the provided {@link AudioStream audio stream}
*/
static String cacheKeyOf(final StreamInfo info, final AudioStream audioStream) {
final boolean averageBitrateUnknown = audioStream.getAverageBitrate() == UNKNOWN_BITRATE;
final StringBuilder cacheKey = commonCacheKeyOf(info, audioStream, averageBitrateUnknown);
// averageBitrate (if known)
if (!averageBitrateUnknown) {
cacheKey.append(" ");
cacheKey.append(audioStream.getAverageBitrate());
}
return cacheKey.toString();
}
//endregion
//region Live media sources
@Nullable @Nullable
default MediaSource maybeBuildLiveMediaSource(@NonNull final PlayerDataSource dataSource, static MediaSource maybeBuildLiveMediaSource(final PlayerDataSource dataSource,
@NonNull final StreamInfo info) { final StreamInfo info) {
final StreamType streamType = info.getStreamType(); if (!StreamTypeUtil.isLiveStream(info.getStreamType())) {
if (!StreamTypeUtil.isLiveStream(streamType)) {
return null; return null;
} }
final StreamInfoTag tag = StreamInfoTag.of(info); try {
if (!info.getHlsUrl().isEmpty()) { final StreamInfoTag tag = StreamInfoTag.of(info);
return buildLiveMediaSource(dataSource, info.getHlsUrl(), C.TYPE_HLS, tag); if (!info.getHlsUrl().isEmpty()) {
} else if (!info.getDashMpdUrl().isEmpty()) { return buildLiveMediaSource(dataSource, info.getHlsUrl(), C.TYPE_HLS, tag);
return buildLiveMediaSource(dataSource, info.getDashMpdUrl(), C.TYPE_DASH, tag); } else if (!info.getDashMpdUrl().isEmpty()) {
return buildLiveMediaSource(dataSource, info.getDashMpdUrl(), C.TYPE_DASH, tag);
}
} catch (final Exception e) {
Log.w(TAG, "Error when generating live media source, falling back to standard sources",
e);
} }
return null; return null;
} }
@NonNull static MediaSource buildLiveMediaSource(final PlayerDataSource dataSource,
default MediaSource buildLiveMediaSource(@NonNull final PlayerDataSource dataSource, final String sourceUrl,
@NonNull final String sourceUrl, @C.ContentType final int type,
@C.ContentType final int type, final MediaItemTag metadata) throws ResolverException {
@NonNull final MediaItemTag metadata) {
final MediaSource.Factory factory; final MediaSource.Factory factory;
switch (type) { switch (type) {
case C.TYPE_SS: case C.TYPE_SS:
@ -56,8 +199,10 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
case C.TYPE_HLS: case C.TYPE_HLS:
factory = dataSource.getLiveHlsMediaSourceFactory(); factory = dataSource.getLiveHlsMediaSourceFactory();
break; break;
case C.TYPE_OTHER:
case C.TYPE_RTSP:
default: default:
throw new IllegalStateException("Unsupported type: " + type); throw new ResolverException("Unsupported type: " + type);
} }
return factory.createMediaSource( return factory.createMediaSource(
@ -67,46 +212,317 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
.setLiveConfiguration( .setLiveConfiguration(
new MediaItem.LiveConfiguration.Builder() new MediaItem.LiveConfiguration.Builder()
.setTargetOffsetMs(LIVE_STREAM_EDGE_GAP_MILLIS) .setTargetOffsetMs(LIVE_STREAM_EDGE_GAP_MILLIS)
.build() .build())
) .build());
.build()
);
} }
//endregion
@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; //region Generic media sources
switch (type) { static MediaSource buildMediaSource(final PlayerDataSource dataSource,
case C.TYPE_SS: final Stream stream,
factory = dataSource.getLiveSsMediaSourceFactory(); final StreamInfo streamInfo,
break; final String cacheKey,
case C.TYPE_DASH: final MediaItemTag metadata) throws ResolverException {
factory = dataSource.getDashMediaSourceFactory(); if (streamInfo.getService() == ServiceList.YouTube) {
break; return createYoutubeMediaSource(stream, streamInfo, dataSource, cacheKey, metadata);
case C.TYPE_HLS:
factory = dataSource.getHlsMediaSourceFactory();
break;
case C.TYPE_OTHER:
factory = dataSource.getExtractorMediaSourceFactory();
break;
default:
throw new IllegalStateException("Unsupported type: " + type);
} }
return factory.createMediaSource( final DeliveryMethod deliveryMethod = stream.getDeliveryMethod();
new MediaItem.Builder() switch (deliveryMethod) {
.setTag(metadata) case PROGRESSIVE_HTTP:
.setUri(uri) return buildProgressiveMediaSource(dataSource, stream, cacheKey, metadata);
.setCustomCacheKey(cacheKey) case DASH:
.build() 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 ResolverException("Unsupported delivery type: " + deliveryMethod);
}
} }
private static ProgressiveMediaSource buildProgressiveMediaSource(
final PlayerDataSource dataSource,
final Stream stream,
final String cacheKey,
final MediaItemTag metadata) throws ResolverException {
if (!stream.isUrl()) {
throw new ResolverException("Non URI progressive contents are not supported");
}
throwResolverExceptionIfUrlNullOrEmpty(stream.getContent());
return dataSource.getProgressiveMediaSourceFactory().createMediaSource(
new MediaItem.Builder()
.setTag(metadata)
.setUri(Uri.parse(stream.getContent()))
.setCustomCacheKey(cacheKey)
.build());
}
private static DashMediaSource buildDashMediaSource(final PlayerDataSource dataSource,
final Stream stream,
final String cacheKey,
final MediaItemTag metadata)
throws ResolverException {
if (stream.isUrl()) {
throwResolverExceptionIfUrlNullOrEmpty(stream.getContent());
return dataSource.getDashMediaSourceFactory().createMediaSource(
new MediaItem.Builder()
.setTag(metadata)
.setUri(Uri.parse(stream.getContent()))
.setCustomCacheKey(cacheKey)
.build());
}
try {
return dataSource.getDashMediaSourceFactory().createMediaSource(
createDashManifest(stream.getContent(), stream),
new MediaItem.Builder()
.setTag(metadata)
.setUri(manifestUrlToUri(stream.getManifestUrl()))
.setCustomCacheKey(cacheKey)
.build());
} catch (final IOException e) {
throw new ResolverException(
"Could not create a DASH media source/manifest from the manifest text", e);
}
}
private static DashManifest createDashManifest(final String manifestContent,
final Stream stream) throws IOException {
return new DashManifestParser().parse(manifestUrlToUri(stream.getManifestUrl()),
new ByteArrayInputStream(manifestContent.getBytes(StandardCharsets.UTF_8)));
}
private static HlsMediaSource buildHlsMediaSource(final PlayerDataSource dataSource,
final Stream stream,
final String cacheKey,
final MediaItemTag metadata)
throws ResolverException {
if (stream.isUrl()) {
throwResolverExceptionIfUrlNullOrEmpty(stream.getContent());
return dataSource.getHlsMediaSourceFactory(null).createMediaSource(
new MediaItem.Builder()
.setTag(metadata)
.setUri(Uri.parse(stream.getContent()))
.setCustomCacheKey(cacheKey)
.build());
}
final NonUriHlsDataSourceFactory.Builder hlsDataSourceFactoryBuilder =
new NonUriHlsDataSourceFactory.Builder();
hlsDataSourceFactoryBuilder.setPlaylistString(stream.getContent());
return dataSource.getHlsMediaSourceFactory(hlsDataSourceFactoryBuilder)
.createMediaSource(new MediaItem.Builder()
.setTag(metadata)
.setUri(manifestUrlToUri(stream.getManifestUrl()))
.setCustomCacheKey(cacheKey)
.build());
}
private static SsMediaSource buildSSMediaSource(final PlayerDataSource dataSource,
final Stream stream,
final String cacheKey,
final MediaItemTag metadata)
throws ResolverException {
if (stream.isUrl()) {
throwResolverExceptionIfUrlNullOrEmpty(stream.getContent());
return dataSource.getSSMediaSourceFactory().createMediaSource(
new MediaItem.Builder()
.setTag(metadata)
.setUri(Uri.parse(stream.getContent()))
.setCustomCacheKey(cacheKey)
.build());
}
final Uri manifestUri = manifestUrlToUri(stream.getManifestUrl());
final SsManifest smoothStreamingManifest;
try {
final ByteArrayInputStream smoothStreamingManifestInput = new ByteArrayInputStream(
stream.getContent().getBytes(StandardCharsets.UTF_8));
smoothStreamingManifest = new SsManifestParser().parse(manifestUri,
smoothStreamingManifestInput);
} catch (final IOException e) {
throw new ResolverException("Error when parsing manual SS manifest", e);
}
return dataSource.getSSMediaSourceFactory().createMediaSource(
smoothStreamingManifest,
new MediaItem.Builder()
.setTag(metadata)
.setUri(manifestUri)
.setCustomCacheKey(cacheKey)
.build());
}
//endregion
//region YouTube media sources
private static MediaSource createYoutubeMediaSource(final Stream stream,
final StreamInfo streamInfo,
final PlayerDataSource dataSource,
final String cacheKey,
final MediaItemTag metadata)
throws ResolverException {
if (!(stream instanceof AudioStream || stream instanceof VideoStream)) {
throw new ResolverException("Generation of YouTube DASH manifest for "
+ stream.getClass().getSimpleName() + " is not supported");
}
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 | IOException | NullPointerException e) {
throw new ResolverException(
"Error when generating the DASH manifest of YouTube ended live stream", e);
}
} else {
throw new ResolverException(
"DASH manifest generation of YouTube livestreams is not supported");
}
}
private static MediaSource createYoutubeMediaSourceOfVideoStreamType(
final PlayerDataSource dataSource,
final Stream stream,
final StreamInfo streamInfo,
final String cacheKey,
final MediaItemTag metadata) throws ResolverException {
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 | IOException | NullPointerException e) {
Log.e(TAG,
"Error when generating the DASH manifest of YouTube OTF stream", e);
throw new ResolverException(
"Error when generating the DASH manifest of YouTube OTF stream", e);
}
case HLS:
return dataSource.getYoutubeHlsMediaSourceFactory().createMediaSource(
new MediaItem.Builder()
.setTag(metadata)
.setUri(Uri.parse(stream.getContent()))
.setCustomCacheKey(cacheKey)
.build());
default:
throw new ResolverException("Unsupported delivery method for YouTube contents: "
+ deliveryMethod);
}
}
private static DashMediaSource buildYoutubeManualDashMediaSource(
final PlayerDataSource dataSource,
final DashManifest dashManifest,
final Stream stream,
final String cacheKey,
final MediaItemTag metadata) {
return dataSource.getYoutubeDashMediaSourceFactory().createMediaSource(dashManifest,
new MediaItem.Builder()
.setTag(metadata)
.setUri(Uri.parse(stream.getContent()))
.setCustomCacheKey(cacheKey)
.build());
}
private static ProgressiveMediaSource buildYoutubeProgressiveMediaSource(
final PlayerDataSource dataSource,
final Stream stream,
final String cacheKey,
final MediaItemTag metadata) {
return dataSource.getYoutubeProgressiveMediaSourceFactory()
.createMediaSource(new MediaItem.Builder()
.setTag(metadata)
.setUri(Uri.parse(stream.getContent()))
.setCustomCacheKey(cacheKey)
.build());
}
//endregion
//region Utils
private static Uri manifestUrlToUri(final String manifestUrl) {
return Uri.parse(Objects.requireNonNullElse(manifestUrl, ""));
}
private static void throwResolverExceptionIfUrlNullOrEmpty(@Nullable final String url)
throws ResolverException {
if (url == null) {
throw new ResolverException("Null stream URL");
} else if (url.isEmpty()) {
throw new ResolverException("Empty stream URL");
}
}
//endregion
//region Resolver exception
final class ResolverException extends Exception {
public ResolverException(final String message) {
super(message);
}
public ResolverException(final String message, final Throwable cause) {
super(message, cause);
}
}
//endregion
} }

View File

@ -2,6 +2,7 @@ package org.schabi.newpipe.player.resolver;
import android.content.Context; import android.content.Context;
import android.net.Uri; import android.net.Uri;
import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@ -27,8 +28,12 @@ import java.util.List;
import java.util.Optional; import java.util.Optional;
import static com.google.android.exoplayer2.C.TIME_UNSET; import static com.google.android.exoplayer2.C.TIME_UNSET;
import static org.schabi.newpipe.util.ListHelper.getUrlAndNonTorrentStreams;
import static org.schabi.newpipe.util.ListHelper.getNonTorrentStreams;
public class VideoPlaybackResolver implements PlaybackResolver { public class VideoPlaybackResolver implements PlaybackResolver {
private static final String TAG = VideoPlaybackResolver.class.getSimpleName();
@NonNull @NonNull
private final Context context; private final Context context;
@NonNull @NonNull
@ -57,7 +62,7 @@ public class VideoPlaybackResolver implements PlaybackResolver {
@Override @Override
@Nullable @Nullable
public MediaSource resolve(@NonNull final StreamInfo info) { public MediaSource resolve(@NonNull final StreamInfo info) {
final MediaSource liveSource = maybeBuildLiveMediaSource(dataSource, info); final MediaSource liveSource = PlaybackResolver.maybeBuildLiveMediaSource(dataSource, info);
if (liveSource != null) { if (liveSource != null) {
streamSourceType = SourceType.LIVE_STREAM; streamSourceType = SourceType.LIVE_STREAM;
return liveSource; return liveSource;
@ -66,40 +71,51 @@ public class VideoPlaybackResolver implements PlaybackResolver {
final List<MediaSource> mediaSources = new ArrayList<>(); final List<MediaSource> mediaSources = new ArrayList<>();
// Create video stream source // Create video stream source
final List<VideoStream> videos = ListHelper.getSortedStreamVideosList(context, final List<VideoStream> videoStreamsList = ListHelper.getSortedStreamVideosList(context,
info.getVideoStreams(), info.getVideoOnlyStreams(), false, true); getNonTorrentStreams(info.getVideoStreams()),
getNonTorrentStreams(info.getVideoOnlyStreams()), false, true);
final int index; final int index;
if (videos.isEmpty()) { if (videoStreamsList.isEmpty()) {
index = -1; index = -1;
} else if (playbackQuality == null) { } else if (playbackQuality == null) {
index = qualityResolver.getDefaultResolutionIndex(videos); index = qualityResolver.getDefaultResolutionIndex(videoStreamsList);
} else { } else {
index = qualityResolver.getOverrideResolutionIndex(videos, getPlaybackQuality()); index = qualityResolver.getOverrideResolutionIndex(videoStreamsList,
getPlaybackQuality());
} }
final MediaItemTag tag = StreamInfoTag.of(info, videos, index); final MediaItemTag tag = StreamInfoTag.of(info, videoStreamsList, index);
@Nullable final VideoStream video = tag.getMaybeQuality() @Nullable final VideoStream video = tag.getMaybeQuality()
.map(MediaItemTag.Quality::getSelectedVideoStream) .map(MediaItemTag.Quality::getSelectedVideoStream)
.orElse(null); .orElse(null);
if (video != null) { if (video != null) {
final MediaSource streamSource = buildMediaSource(dataSource, video.getUrl(), try {
PlayerHelper.cacheKeyOf(info, video), final MediaSource streamSource = PlaybackResolver.buildMediaSource(
MediaFormat.getSuffixById(video.getFormatId()), tag); dataSource, video, info, PlaybackResolver.cacheKeyOf(info, video), tag);
mediaSources.add(streamSource); mediaSources.add(streamSource);
} catch (final ResolverException e) {
Log.e(TAG, "Unable to create video source", e);
return null;
}
} }
// Create optional audio stream source // Create optional audio stream source
final List<AudioStream> audioStreams = info.getAudioStreams(); final List<AudioStream> audioStreams = getNonTorrentStreams(info.getAudioStreams());
final AudioStream audio = audioStreams.isEmpty() ? null : audioStreams.get( final AudioStream audio = audioStreams.isEmpty() ? null : audioStreams.get(
ListHelper.getDefaultAudioFormat(context, audioStreams)); ListHelper.getDefaultAudioFormat(context, audioStreams));
// Use the audio stream if there is no video stream, or // Use the audio stream if there is no video stream, or
// Merge with audio stream in case if video does not contain audio // merge with audio stream in case if video does not contain audio
if (audio != null && (video == null || video.isVideoOnly)) { if (audio != null && (video == null || video.isVideoOnly())) {
final MediaSource audioSource = buildMediaSource(dataSource, audio.getUrl(), try {
PlayerHelper.cacheKeyOf(info, audio), final MediaSource audioSource = PlaybackResolver.buildMediaSource(
MediaFormat.getSuffixById(audio.getFormatId()), tag); dataSource, audio, info, PlaybackResolver.cacheKeyOf(info, audio), tag);
mediaSources.add(audioSource); mediaSources.add(audioSource);
streamSourceType = SourceType.VIDEO_WITH_SEPARATED_AUDIO; streamSourceType = SourceType.VIDEO_WITH_SEPARATED_AUDIO;
} catch (final ResolverException e) {
Log.e(TAG, "Unable to create audio source", e);
return null;
}
} else { } else {
streamSourceType = SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY; streamSourceType = SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY;
} }
@ -108,36 +124,39 @@ public class VideoPlaybackResolver implements PlaybackResolver {
if (mediaSources.isEmpty()) { if (mediaSources.isEmpty()) {
return null; return null;
} }
// Below are auxiliary media sources // Below are auxiliary media sources
// Create subtitle sources // Create subtitle sources
if (info.getSubtitles() != null) { final List<SubtitlesStream> subtitlesStreams = info.getSubtitles();
for (final SubtitlesStream subtitle : info.getSubtitles()) { if (subtitlesStreams != null) {
final String mimeType = PlayerHelper.subtitleMimeTypesOf(subtitle.getFormat()); // Torrent and non URL subtitles are not supported by ExoPlayer
if (mimeType == null) { final List<SubtitlesStream> nonTorrentAndUrlStreams = getUrlAndNonTorrentStreams(
continue; subtitlesStreams);
for (final SubtitlesStream subtitle : nonTorrentAndUrlStreams) {
final MediaFormat mediaFormat = subtitle.getFormat();
if (mediaFormat != null) {
@C.RoleFlags final int textRoleFlag = subtitle.isAutoGenerated()
? C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND
: C.ROLE_FLAG_CAPTION;
final MediaItem.SubtitleConfiguration textMediaItem =
new MediaItem.SubtitleConfiguration.Builder(
Uri.parse(subtitle.getContent()))
.setMimeType(mediaFormat.getMimeType())
.setRoleFlags(textRoleFlag)
.setLanguage(PlayerHelper.captionLanguageOf(context, subtitle))
.build();
final MediaSource textSource = dataSource.getSingleSampleMediaSourceFactory()
.createMediaSource(textMediaItem, TIME_UNSET);
mediaSources.add(textSource);
} }
final @C.RoleFlags int textRoleFlag = subtitle.isAutoGenerated()
? C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND
: C.ROLE_FLAG_CAPTION;
final MediaItem.SubtitleConfiguration textMediaItem =
new MediaItem.SubtitleConfiguration.Builder(Uri.parse(subtitle.getUrl()))
.setMimeType(mimeType)
.setRoleFlags(textRoleFlag)
.setLanguage(PlayerHelper.captionLanguageOf(context, subtitle))
.build();
final MediaSource textSource = dataSource
.getSampleMediaSourceFactory()
.createMediaSource(textMediaItem, TIME_UNSET);
mediaSources.add(textSource);
} }
} }
if (mediaSources.size() == 1) { if (mediaSources.size() == 1) {
return mediaSources.get(0); return mediaSources.get(0);
} else { } else {
return new MergingMediaSource(mediaSources.toArray( return new MergingMediaSource(true, mediaSources.toArray(new MediaSource[0]));
new MediaSource[0]));
} }
} }

View File

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

View File

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

View File

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

View File

@ -1,7 +1,5 @@
package org.schabi.newpipe.util; package org.schabi.newpipe.util;
import static org.schabi.newpipe.extractor.stream.StreamType.AUDIO_LIVE_STREAM;
import static org.schabi.newpipe.extractor.stream.StreamType.LIVE_STREAM;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
import android.content.Context; import android.content.Context;
@ -49,8 +47,8 @@ public final class SparseItemUtil {
public static void fetchItemInfoIfSparse(@NonNull final Context context, public static void fetchItemInfoIfSparse(@NonNull final Context context,
@NonNull final StreamInfoItem item, @NonNull final StreamInfoItem item,
@NonNull final Consumer<SinglePlayQueue> callback) { @NonNull final Consumer<SinglePlayQueue> callback) {
if (((item.getStreamType() == LIVE_STREAM || item.getStreamType() == AUDIO_LIVE_STREAM) if ((StreamTypeUtil.isLiveStream(item.getStreamType()) || item.getDuration() >= 0)
|| item.getDuration() >= 0) && !isNullOrEmpty(item.getUploaderUrl())) { && !isNullOrEmpty(item.getUploaderUrl())) {
// if the duration is >= 0 (provided that the item is not a livestream) and there is an // if the duration is >= 0 (provided that the item is not a livestream) and there is an
// uploader url, probably all info is already there, so there is no need to fetch it // uploader url, probably all info is already there, so there is no need to fetch it
callback.accept(new SinglePlayQueue(item)); callback.accept(new SinglePlayQueue(item));

View File

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

View File

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

View File

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

View File

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

View File

@ -82,13 +82,14 @@
android:text="@string/msg_threads" /> android:text="@string/msg_threads" />
<LinearLayout <LinearLayout
android:id="@+id/threads_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@+id/threads_text_view" android:layout_below="@+id/threads_text_view"
android:layout_marginLeft="24dp" android:layout_marginLeft="24dp"
android:layout_marginRight="24dp" android:layout_marginRight="24dp"
android:orientation="horizontal" android:layout_marginBottom="12dp"
android:paddingBottom="12dp"> android:orientation="horizontal">
<org.schabi.newpipe.views.NewPipeTextView <org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/threads_count" android:id="@+id/threads_count"
@ -106,4 +107,16 @@
android:max="31" android:max="31"
android:progress="3" /> android:progress="3" />
</LinearLayout> </LinearLayout>
<org.schabi.newpipe.views.NewPipeTextView
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="12dp"
android:gravity="center"
android:text="@string/streams_not_yet_supported_removed"
android:textSize="12sp" />
</RelativeLayout> </RelativeLayout>

View File

@ -740,4 +740,11 @@
<string name="you_successfully_subscribed">You now subscribed to this channel</string> <string name="you_successfully_subscribed">You now subscribed to this channel</string>
<string name="enumeration_comma">,</string> <string name="enumeration_comma">,</string>
<string name="toggle_all">Toggle all</string> <string name="toggle_all">Toggle all</string>
<string name="streams_not_yet_supported_removed">Streams which are not yet supported by the downloader are not shown</string>
<string name="selected_stream_external_player_not_supported">The selected stream is not supported by external players</string>
<string name="no_audio_streams_available_for_external_players">No audio streams are available for external players</string>
<string name="no_video_streams_available_for_external_players">No video streams are available for external players</string>
<string name="select_quality_external_players">Select quality for external players</string>
<string name="unknown_format">Unknown format</string>
<string name="unknown_quality">Unknown quality</string>
</resources> </resources>

View File

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