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

View file

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

View file

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

View file

@ -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.Companion.STREAM_ID
import org.schabi.newpipe.extractor.stream.StreamType
import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_LIVE_STREAM
import org.schabi.newpipe.extractor.stream.StreamType.LIVE_STREAM
import org.schabi.newpipe.util.StreamTypeUtil
import java.time.OffsetDateTime
@Dao
@ -91,8 +90,7 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
?: throw IllegalStateException("Stream cannot be null just after insertion.")
newerStream.uid = existentMinimalStream.uid
val isNewerStreamLive = newerStream.streamType == AUDIO_LIVE_STREAM || newerStream.streamType == LIVE_STREAM
if (!isNewerStreamLive) {
if (!StreamTypeUtil.isLiveStream(newerStream.streamType)) {
// 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.

View file

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

View file

@ -31,6 +31,7 @@ import android.view.WindowManager;
import android.view.animation.DecelerateInterpolator;
import android.widget.FrameLayout;
import android.widget.RelativeLayout;
import android.widget.Toast;
import androidx.annotation.AttrRes;
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.PermissionHelper;
import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.StreamTypeUtil;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.util.external_communication.KoreUtils;
import org.schabi.newpipe.util.external_communication.ShareUtils;
@ -121,6 +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.playqueue.PlayQueueItem.RECOVERY_UNSET;
import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
import static org.schabi.newpipe.util.ListHelper.getUrlAndNonTorrentStreams;
public final class VideoDetailFragment
extends BaseStateFragment<StreamInfo>
@ -186,8 +189,6 @@ public final class VideoDetailFragment
@Nullable
private Disposable positionSubscriber = null;
private List<VideoStream> sortedVideoStreams;
private int selectedVideoStreamIndex = -1;
private BottomSheetBehavior<FrameLayout> bottomSheetBehavior;
private BroadcastReceiver broadcastReceiver;
@ -1092,9 +1093,6 @@ public final class VideoDetailFragment
}
private void openBackgroundPlayer(final boolean append) {
final AudioStream audioStream = currentInfo.getAudioStreams()
.get(ListHelper.getDefaultAudioFormat(activity, currentInfo.getAudioStreams()));
final boolean useExternalAudioPlayer = PreferenceManager
.getDefaultSharedPreferences(activity)
.getBoolean(activity.getString(R.string.use_external_audio_player_key), false);
@ -1109,7 +1107,17 @@ public final class VideoDetailFragment
if (!useExternalAudioPlayer) {
openNormalBackgroundPlayer(append);
} 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.detailSecondaryControlPanel.setVisibility(View.GONE);
sortedVideoStreams = ListHelper.getSortedStreamVideosList(
activity,
info.getVideoStreams(),
info.getVideoOnlyStreams(),
false,
false);
selectedVideoStreamIndex = ListHelper
.getDefaultResolutionIndex(activity, sortedVideoStreams);
updateProgressInfo(info);
initThumbnailViews(info);
showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView,
@ -1645,8 +1645,8 @@ public final class VideoDetailFragment
}
}
binding.detailControlsDownload.setVisibility(info.getStreamType() == StreamType.LIVE_STREAM
|| info.getStreamType() == StreamType.AUDIO_LIVE_STREAM ? View.GONE : View.VISIBLE);
binding.detailControlsDownload.setVisibility(
StreamTypeUtil.isLiveStream(info.getStreamType()) ? View.GONE : View.VISIBLE);
binding.detailControlsBackground.setVisibility(info.getAudioStreams().isEmpty()
? View.GONE : View.VISIBLE);
@ -1687,12 +1687,7 @@ public final class VideoDetailFragment
}
try {
final DownloadDialog downloadDialog = DownloadDialog.newInstance(currentInfo);
downloadDialog.setVideoStreams(sortedVideoStreams);
downloadDialog.setAudioStreams(currentInfo.getAudioStreams());
downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex);
downloadDialog.setSubtitleStreams(currentInfo.getSubtitles());
final DownloadDialog downloadDialog = new DownloadDialog(activity, currentInfo);
downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
} catch (final Exception e) {
ErrorUtil.showSnackbar(activity, new ErrorInfo(e, UserAction.DOWNLOAD_OPEN_DIALOG,
@ -1722,8 +1717,7 @@ public final class VideoDetailFragment
binding.detailPositionView.setVisibility(View.GONE);
// TODO: Remove this check when separation of concerns is done.
// (live streams weren't getting updated because they are mixed)
if (!info.getStreamType().equals(StreamType.LIVE_STREAM)
&& !info.getStreamType().equals(StreamType.AUDIO_LIVE_STREAM)) {
if (!StreamTypeUtil.isLiveStream(info.getStreamType())) {
return;
}
} else {
@ -2151,25 +2145,52 @@ public final class VideoDetailFragment
}
private void showExternalPlaybackDialog() {
if (sortedVideoStreams == null) {
if (currentInfo == null) {
return;
}
final CharSequence[] resolutions = new CharSequence[sortedVideoStreams.size()];
for (int i = 0; i < sortedVideoStreams.size(); i++) {
resolutions[i] = sortedVideoStreams.get(i).getResolution();
}
final AlertDialog.Builder builder = new AlertDialog.Builder(activity)
.setNegativeButton(R.string.cancel, null)
.setNeutralButton(R.string.open_in_browser, (dialog, i) ->
ShareUtils.openUrlInBrowser(requireActivity(), url)
final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setTitle(R.string.select_quality_external_players);
builder.setNeutralButton(R.string.open_in_browser, (dialog, i) ->
ShareUtils.openUrlInBrowser(requireActivity(), url));
final List<VideoStream> videoStreamsForExternalPlayers =
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) {
builder.setSingleChoiceItems(resolutions, selectedVideoStreamIndex, (dialog, i) -> {
dialog.dismiss();
startOnExternalPlayer(activity, currentInfo, sortedVideoStreams.get(i));
}
);
if (videoStreamsForExternalPlayers.isEmpty()) {
builder.setMessage(R.string.no_video_streams_available_for_external_players);
builder.setPositiveButton(R.string.ok, null);
} 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();
}

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.StreamType;
import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.util.StreamTypeUtil;
import org.schabi.newpipe.util.external_communication.KoreUtils;
import java.util.ArrayList;
@ -269,8 +270,7 @@ public final class InfoItemDialog {
*/
public Builder addStartHereEntries() {
addEntry(StreamDialogDefaultEntry.START_HERE_ON_BACKGROUND);
if (infoItem.getStreamType() != StreamType.AUDIO_STREAM
&& infoItem.getStreamType() != StreamType.AUDIO_LIVE_STREAM) {
if (!StreamTypeUtil.isAudio(infoItem.getStreamType())) {
addEntry(StreamDialogDefaultEntry.START_HERE_ON_POPUP);
}
return this;
@ -285,9 +285,7 @@ public final class InfoItemDialog {
final boolean isWatchHistoryEnabled = PreferenceManager
.getDefaultSharedPreferences(context)
.getBoolean(context.getString(R.string.enable_watch_history_key), false);
if (isWatchHistoryEnabled
&& infoItem.getStreamType() != StreamType.LIVE_STREAM
&& infoItem.getStreamType() != StreamType.AUDIO_LIVE_STREAM) {
if (isWatchHistoryEnabled && !StreamTypeUtil.isLiveStream(infoItem.getStreamType())) {
addEntry(StreamDialogDefaultEntry.MARK_AS_WATCHED);
}
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.extractor.InfoItem;
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.ktx.ViewUtils;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.StreamTypeUtil;
import org.schabi.newpipe.views.AnimatedProgressBar;
import java.util.concurrent.TimeUnit;
@ -70,8 +70,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
} else {
itemProgressView.setVisibility(View.GONE);
}
} else if (item.getStreamType() == StreamType.LIVE_STREAM
|| item.getStreamType() == StreamType.AUDIO_LIVE_STREAM) {
} else if (StreamTypeUtil.isLiveStream(item.getStreamType())) {
itemDurationView.setText(R.string.duration_live);
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(),
R.color.live_duration_background_color));
@ -96,9 +95,10 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
case VIDEO_STREAM:
case LIVE_STREAM:
case AUDIO_LIVE_STREAM:
case POST_LIVE_STREAM:
case POST_LIVE_AUDIO_STREAM:
enableLongClick(item);
break;
case FILE:
case NONE:
default:
disableLongClick();
@ -114,7 +114,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
final StreamStateEntity state
= historyRecordManager.loadStreamState(infoItem).blockingGet()[0];
if (state != null && item.getDuration() > 0
&& item.getStreamType() != StreamType.LIVE_STREAM) {
&& !StreamTypeUtil.isLiveStream(item.getStreamType())) {
itemProgressView.setMax((int) item.getDuration());
if (itemProgressView.getVisibility() == View.VISIBLE) {
itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS

View file

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

View file

@ -150,7 +150,6 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.Target;
import org.schabi.newpipe.DownloaderImpl;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.stream.model.StreamEntity;
@ -429,7 +428,7 @@ public final class Player implements
setupBroadcastReceiver();
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());
loadController = new LoadController();
renderFactory = new DefaultRenderersFactory(context);
@ -1744,24 +1743,9 @@ public final class Player implements
if (exoPlayerIsNull()) {
return;
}
// Use duration of currentItem for non-live streams,
// because HLS streams are fragmented
// and thus the whole duration is not available to the player
// TODO: revert #6307 when introducing proper HLS support
final int duration;
if (currentItem != null
&& !StreamTypeUtil.isLiveStream(currentItem.getStreamType())
) {
// convert seconds to milliseconds
duration = (int) (currentItem.getDuration() * 1000);
} else {
duration = (int) simpleExoPlayer.getDuration();
}
onUpdateProgress(
Math.max((int) simpleExoPlayer.getCurrentPosition(), 0),
duration,
simpleExoPlayer.getBufferedPercentage()
);
onUpdateProgress(Math.max((int) simpleExoPlayer.getCurrentPosition(), 0),
(int) simpleExoPlayer.getDuration(), simpleExoPlayer.getBufferedPercentage());
}
private Disposable getProgressUpdateDisposable() {
@ -3399,6 +3383,7 @@ public final class Player implements
switch (info.getStreamType()) {
case AUDIO_STREAM:
case POST_LIVE_AUDIO_STREAM:
binding.surfaceView.setVisibility(View.GONE);
binding.endScreen.setVisibility(View.VISIBLE);
binding.playbackEndTime.setVisibility(View.VISIBLE);
@ -3417,6 +3402,7 @@ public final class Player implements
break;
case VIDEO_STREAM:
case POST_LIVE_STREAM:
if (currentMetadata == null
|| !currentMetadata.getMaybeQuality().isPresent()
|| (info.getVideoStreams().isEmpty()
@ -3484,10 +3470,10 @@ public final class Player implements
for (int i = 0; i < availableStreams.size(); i++) {
final VideoStream videoStream = availableStreams.get(i);
qualityPopupMenu.getMenu().add(POPUP_MENU_ID_QUALITY, i, Menu.NONE, MediaFormat
.getNameById(videoStream.getFormatId()) + " " + videoStream.resolution);
.getNameById(videoStream.getFormatId()) + " " + videoStream.getResolution());
}
if (getSelectedVideoStream() != null) {
binding.qualityTextView.setText(getSelectedVideoStream().resolution);
binding.qualityTextView.setText(getSelectedVideoStream().getResolution());
}
qualityPopupMenu.setOnMenuItemClickListener(this);
qualityPopupMenu.setOnDismissListener(this);
@ -3605,7 +3591,7 @@ public final class Player implements
}
saveStreamProgressState(); //TODO added, check if good
final String newResolution = availableStreams.get(menuItemIndex).resolution;
final String newResolution = availableStreams.get(menuItemIndex).getResolution();
setRecovery();
setPlaybackQuality(newResolution);
reloadPlayQueueManager();
@ -3633,7 +3619,7 @@ public final class Player implements
}
isSomePopupMenuVisible = false; //TODO check if this works
if (getSelectedVideoStream() != null) {
binding.qualityTextView.setText(getSelectedVideoStream().resolution);
binding.qualityTextView.setText(getSelectedVideoStream().getResolution());
}
if (isPlaying()) {
hideControls(DEFAULT_CONTROLS_DURATION, 0);
@ -4248,9 +4234,7 @@ public final class Player implements
if (playQueueManagerReloadingNeeded(sourceType, info, getVideoRendererIndex())) {
reloadPlayQueueManager();
} else {
final StreamType streamType = info.getStreamType();
if (streamType == StreamType.AUDIO_STREAM
|| streamType == StreamType.AUDIO_LIVE_STREAM) {
if (StreamTypeUtil.isAudio(info.getStreamType())) {
// Nothing to do more than setting the recovery position
setRecovery();
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:
*
* <ul>
* <li>the content is an {@link StreamType#AUDIO_STREAM audio stream} or an
* {@link StreamType#AUDIO_LIVE_STREAM audio live stream};</li>
* <li>the content is an {@link StreamType#AUDIO_STREAM audio stream}, an
* {@link StreamType#AUDIO_LIVE_STREAM audio live stream}, or a
* {@link StreamType#POST_LIVE_AUDIO_STREAM ended audio live stream};</li>
* <li>the content is a {@link StreamType#LIVE_STREAM live stream} and the source type is a
* {@link SourceType#LIVE_STREAM live source};</li>
* <li>the content's source is {@link SourceType#VIDEO_WITH_SEPARATED_AUDIO a video stream
* with a separated audio source} or has no audio-only streams available <b>and</b> is a
* {@link StreamType#LIVE_STREAM live stream} or a
* {@link StreamType#VIDEO_STREAM video stream}, an
* {@link StreamType#POST_LIVE_STREAM ended live stream}, or a
* {@link StreamType#LIVE_STREAM live stream}.
* </li>
* </ul>
@ -4307,18 +4293,17 @@ public final class Player implements
@NonNull final StreamInfo streamInfo,
final int videoRendererIndex) {
final StreamType streamType = streamInfo.getStreamType();
final boolean isStreamTypeAudio = StreamTypeUtil.isAudio(streamType);
if (videoRendererIndex == RENDERER_UNAVAILABLE && streamType != StreamType.AUDIO_STREAM
&& streamType != StreamType.AUDIO_LIVE_STREAM) {
if (videoRendererIndex == RENDERER_UNAVAILABLE && !isStreamTypeAudio) {
return true;
}
// The content is an audio stream, an audio live stream, or a live stream with a live
// source: it's not needed to reload the play queue manager because the stream source will
// be the same
if ((streamType == StreamType.AUDIO_STREAM || streamType == StreamType.AUDIO_LIVE_STREAM)
|| (streamType == StreamType.LIVE_STREAM
&& sourceType == SourceType.LIVE_STREAM)) {
if (isStreamTypeAudio || (streamType == StreamType.LIVE_STREAM
&& sourceType == SourceType.LIVE_STREAM)) {
return false;
}
@ -4331,8 +4316,8 @@ public final class Player implements
|| (sourceType == SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY
&& isNullOrEmpty(streamInfo.getAudioStreams()))) {
// It's not needed to reload the play queue manager only if the content's stream type
// is a video stream or a live stream
return streamType != StreamType.VIDEO_STREAM && streamType != StreamType.LIVE_STREAM;
// is a video stream, a live stream or an ended live stream
return !StreamTypeUtil.isVideo(streamType);
}
// 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;
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.DefaultDataSource;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
import com.google.android.exoplayer2.upstream.FileDataSource;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.upstream.cache.CacheDataSink;
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 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 {
private static final String TAG = "CacheFactory";
private static final String CACHE_FOLDER_NAME = "exoplayer";
private static final int CACHE_FLAGS = CacheDataSource.FLAG_BLOCK_ON_CACHE
| CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR;
private final DataSource.Factory dataSourceFactory;
private final File cacheDir;
private final long maxFileSize;
// Creating cache on every instance may cause problems with multiple players when
// sources are not ExtractorMediaSource
// see: https://stackoverflow.com/questions/28700391/using-cache-in-exoplayer
// todo: make this a singleton?
private static 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));
}
CacheFactory(final Context context,
final TransferListener transferListener,
final SimpleCache cache,
final DataSource.Factory upstreamDataSourceFactory) {
this.context = context;
this.transferListener = transferListener;
this.cache = cache;
this.upstreamDataSourceFactory = upstreamDataSourceFactory;
}
@NonNull
@Override
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 CacheDataSink dataSink = new CacheDataSink(cache, maxFileSize);
final CacheDataSink dataSink
= new CacheDataSink(cache, PlayerHelper.getPreferredFileSize());
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;
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.SingleSampleMediaSource;
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.DefaultDataSource;
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.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 static final String TAG = PlayerDataSource.class.getSimpleName();
public static final int LIVE_STREAM_EDGE_GAP_MILLIS = 10000;
@ -29,79 +44,174 @@ public class PlayerDataSource {
* early.
*/
private static final double PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT = 15;
private static final int MANIFEST_MINIMUM_RETRY = 5;
private static final int EXTRACTOR_MINIMUM_RETRY = Integer.MAX_VALUE;
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 CacheFactory cacheDataSourceFactory;
public PlayerDataSource(@NonNull final Context context,
@NonNull final String userAgent,
@NonNull final TransferListener transferListener) {
continueLoadingCheckIntervalBytes = PlayerHelper.getProgressiveLoadIntervalBytes(context);
cacheDataSourceFactory = new CacheFactory(context, userAgent, transferListener);
cachelessDataSourceFactory = new DefaultDataSource
.Factory(context, new DefaultHttpDataSource.Factory().setUserAgent(userAgent))
// YouTube-specific Data Source Factories (with cache)
// They use YoutubeHttpDataSource.Factory, with different parameters each
private final CacheFactory ytHlsCacheDataSourceFactory;
private final CacheFactory ytDashCacheDataSourceFactory;
private final CacheFactory ytProgressiveDashCacheDataSourceFactory;
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);
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() {
return new SsMediaSource.Factory(
new DefaultSsChunkSource.Factory(cachelessDataSourceFactory),
cachelessDataSourceFactory
)
.setLoadErrorHandlingPolicy(
new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY))
.setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS);
return getSSMediaSourceFactory().setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS);
}
public HlsMediaSource.Factory getLiveHlsMediaSourceFactory() {
return new HlsMediaSource.Factory(cachelessDataSourceFactory)
.setAllowChunklessPreparation(true)
.setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(
MANIFEST_MINIMUM_RETRY))
.setPlaylistTrackerFactory((dataSourceFactory, loadErrorHandlingPolicy,
playlistParserFactory) ->
new DefaultHlsPlaylistTracker(dataSourceFactory, loadErrorHandlingPolicy,
playlistParserFactory, PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT)
);
playlistParserFactory,
PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT));
}
public DashMediaSource.Factory getLiveDashMediaSourceFactory() {
return new DashMediaSource.Factory(
getDefaultDashChunkSourceFactory(cachelessDataSourceFactory),
cachelessDataSourceFactory
)
.setLoadErrorHandlingPolicy(
new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY));
cachelessDataSourceFactory);
}
//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);
}
public DashMediaSource.Factory getDashMediaSourceFactory() {
return new DashMediaSource.Factory(
getDefaultDashChunkSourceFactory(cacheDataSourceFactory),
cacheDataSourceFactory
);
cacheDataSourceFactory);
}
public ProgressiveMediaSource.Factory getExtractorMediaSourceFactory() {
public ProgressiveMediaSource.Factory getProgressiveMediaSourceFactory() {
return new ProgressiveMediaSource.Factory(cacheDataSourceFactory)
.setContinueLoadingCheckIntervalBytes(continueLoadingCheckIntervalBytes)
.setLoadErrorHandlingPolicy(
new DefaultLoadErrorHandlingPolicy(EXTRACTOR_MINIMUM_RETRY));
.setContinueLoadingCheckIntervalBytes(progressiveLoadIntervalBytes);
}
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);
}
//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.extractor.InfoItem;
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.StreamInfoItem;
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.player.MainPlayer;
import org.schabi.newpipe.player.Player;
@ -110,12 +108,14 @@ public final class PlayerHelper {
int MINIMIZE_ON_EXIT_MODE_POPUP = 2;
}
private PlayerHelper() { }
private PlayerHelper() {
}
////////////////////////////////////////////////////////////////////////////
// Exposed helpers
////////////////////////////////////////////////////////////////////////////
@NonNull
public static String getTimeString(final int milliSeconds) {
final int seconds = (milliSeconds % 60000) / 1000;
final int minutes = (milliSeconds % 3600000) / 60000;
@ -131,15 +131,18 @@ public final class PlayerHelper {
).toString();
}
@NonNull
public static String formatSpeed(final double speed) {
return SPEED_FORMATTER.format(speed);
}
@NonNull
public static String formatPitch(final double pitch) {
return PITCH_FORMATTER.format(pitch);
}
public static String subtitleMimeTypesOf(final MediaFormat format) {
@NonNull
public static String subtitleMimeTypesOf(@NonNull final MediaFormat format) {
switch (format) {
case VTT:
return MimeTypes.TEXT_VTT;
@ -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,
* provide the {@link SinglePlayQueue} consisting of the next video for auto queueing.
@ -233,7 +224,7 @@ public final class PlayerHelper {
return null;
}
if (relatedItems.get(0) != null && relatedItems.get(0) instanceof StreamInfoItem
if (relatedItems.get(0) instanceof StreamInfoItem
&& !urls.contains(relatedItems.get(0).getUrl())) {
return getAutoQueuedSinglePlayQueue((StreamInfoItem) relatedItems.get(0));
}
@ -335,6 +326,7 @@ public final class PlayerHelper {
return 2 * 1024 * 1024L; // ExoPlayer CacheDataSink.MIN_RECOMMENDED_FRAGMENT_SIZE
}
@NonNull
public static ExoTrackSelection.Factory getQualitySelector() {
return new AdaptiveTrackSelection.Factory(
1000,
@ -389,7 +381,7 @@ public final class PlayerHelper {
/**
* @param context the Android context
* @return the screen brightness to use. A value less than 0 (the default) means to use the
* preferred screen brightness
* preferred screen brightness
*/
public static float getScreenBrightness(@NonNull final Context context) {
final SharedPreferences sp = getPreferences(context);
@ -480,7 +472,8 @@ public final class PlayerHelper {
return REPEAT_MODE_ONE;
case REPEAT_MODE_ONE:
return REPEAT_MODE_ALL;
case REPEAT_MODE_ALL: default:
case REPEAT_MODE_ALL:
default:
return REPEAT_MODE_OFF;
}
}
@ -548,7 +541,7 @@ public final class PlayerHelper {
player.getContext().getResources().getDimension(R.dimen.popup_default_width);
final float popupWidth = popupRememberSizeAndPos
? player.getPrefs().getFloat(player.getContext().getString(
R.string.popup_saved_width_key), defaultSize)
R.string.popup_saved_width_key), defaultSize)
: defaultSize;
final float popupHeight = getMinimumVideoHeight(popupWidth);
@ -564,10 +557,10 @@ public final class PlayerHelper {
final int centerY = (int) (player.getScreenHeight() / 2f - popupHeight / 2f);
popupLayoutParams.x = popupRememberSizeAndPos
? player.getPrefs().getInt(player.getContext().getString(
R.string.popup_saved_x_key), centerX) : centerX;
R.string.popup_saved_x_key), centerX) : centerX;
popupLayoutParams.y = popupRememberSizeAndPos
? player.getPrefs().getInt(player.getContext().getString(
R.string.popup_saved_y_key), centerY) : centerY;
R.string.popup_saved_y_key), centerY) : centerY;
return popupLayoutParams;
}

View file

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

View file

@ -1,22 +1,27 @@
package org.schabi.newpipe.player.resolver;
import static org.schabi.newpipe.util.ListHelper.getNonTorrentStreams;
import android.content.Context;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.source.MediaSource;
import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.player.helper.PlayerDataSource;
import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.player.mediaitem.MediaItemTag;
import org.schabi.newpipe.player.mediaitem.StreamInfoTag;
import org.schabi.newpipe.util.ListHelper;
import java.util.List;
public class AudioPlaybackResolver implements PlaybackResolver {
private static final String TAG = AudioPlaybackResolver.class.getSimpleName();
@NonNull
private final Context context;
@NonNull
@ -31,19 +36,27 @@ public class AudioPlaybackResolver implements PlaybackResolver {
@Override
@Nullable
public MediaSource resolve(@NonNull final StreamInfo info) {
final MediaSource liveSource = maybeBuildLiveMediaSource(dataSource, info);
final MediaSource liveSource = PlaybackResolver.maybeBuildLiveMediaSource(dataSource, info);
if (liveSource != null) {
return liveSource;
}
final int index = ListHelper.getDefaultAudioFormat(context, info.getAudioStreams());
final List<AudioStream> audioStreams = getNonTorrentStreams(info.getAudioStreams());
final int index = ListHelper.getDefaultAudioFormat(context, audioStreams);
if (index < 0 || index >= info.getAudioStreams().size()) {
return null;
}
final AudioStream audio = info.getAudioStreams().get(index);
final MediaItemTag tag = StreamInfoTag.of(info);
return buildMediaSource(dataSource, audio.getUrl(), PlayerHelper.cacheKeyOf(info, audio),
MediaFormat.getSuffixById(audio.getFormatId()), tag);
try {
return PlaybackResolver.buildMediaSource(
dataSource, audio, info, 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;
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.text.TextUtils;
import android.util.Log;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.source.dash.DashMediaSource;
import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser;
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
import com.google.android.exoplayer2.source.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.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.mediaitem.MediaItemTag;
import org.schabi.newpipe.player.mediaitem.StreamInfoTag;
import org.schabi.newpipe.util.StreamTypeUtil;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import static org.schabi.newpipe.player.helper.PlayerDataSource.LIVE_STREAM_EDGE_GAP_MILLIS;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
/**
* 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> {
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
default MediaSource maybeBuildLiveMediaSource(@NonNull final PlayerDataSource dataSource,
@NonNull final StreamInfo info) {
final StreamType streamType = info.getStreamType();
if (!StreamTypeUtil.isLiveStream(streamType)) {
static MediaSource maybeBuildLiveMediaSource(final PlayerDataSource dataSource,
final StreamInfo info) {
if (!StreamTypeUtil.isLiveStream(info.getStreamType())) {
return null;
}
final StreamInfoTag tag = StreamInfoTag.of(info);
if (!info.getHlsUrl().isEmpty()) {
return buildLiveMediaSource(dataSource, info.getHlsUrl(), C.TYPE_HLS, tag);
} else if (!info.getDashMpdUrl().isEmpty()) {
return buildLiveMediaSource(dataSource, info.getDashMpdUrl(), C.TYPE_DASH, tag);
try {
final StreamInfoTag tag = StreamInfoTag.of(info);
if (!info.getHlsUrl().isEmpty()) {
return buildLiveMediaSource(dataSource, info.getHlsUrl(), C.TYPE_HLS, 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;
}
@NonNull
default MediaSource buildLiveMediaSource(@NonNull final PlayerDataSource dataSource,
@NonNull final String sourceUrl,
@C.ContentType final int type,
@NonNull final MediaItemTag metadata) {
static MediaSource buildLiveMediaSource(final PlayerDataSource dataSource,
final String sourceUrl,
@C.ContentType final int type,
final MediaItemTag metadata) throws ResolverException {
final MediaSource.Factory factory;
switch (type) {
case C.TYPE_SS:
@ -56,8 +199,10 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
case C.TYPE_HLS:
factory = dataSource.getLiveHlsMediaSourceFactory();
break;
case C.TYPE_OTHER:
case C.TYPE_RTSP:
default:
throw new IllegalStateException("Unsupported type: " + type);
throw new ResolverException("Unsupported type: " + type);
}
return factory.createMediaSource(
@ -67,46 +212,317 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
.setLiveConfiguration(
new MediaItem.LiveConfiguration.Builder()
.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;
switch (type) {
case C.TYPE_SS:
factory = dataSource.getLiveSsMediaSourceFactory();
break;
case C.TYPE_DASH:
factory = dataSource.getDashMediaSourceFactory();
break;
case C.TYPE_HLS:
factory = dataSource.getHlsMediaSourceFactory();
break;
case C.TYPE_OTHER:
factory = dataSource.getExtractorMediaSourceFactory();
break;
default:
throw new IllegalStateException("Unsupported type: " + type);
//region Generic media sources
static MediaSource buildMediaSource(final PlayerDataSource dataSource,
final Stream stream,
final StreamInfo streamInfo,
final String cacheKey,
final MediaItemTag metadata) throws ResolverException {
if (streamInfo.getService() == ServiceList.YouTube) {
return createYoutubeMediaSource(stream, streamInfo, dataSource, cacheKey, metadata);
}
return factory.createMediaSource(
new MediaItem.Builder()
.setTag(metadata)
.setUri(uri)
.setCustomCacheKey(cacheKey)
.build()
);
final DeliveryMethod deliveryMethod = stream.getDeliveryMethod();
switch (deliveryMethod) {
case PROGRESSIVE_HTTP:
return buildProgressiveMediaSource(dataSource, stream, cacheKey, metadata);
case DASH:
return buildDashMediaSource(dataSource, stream, cacheKey, metadata);
case HLS:
return buildHlsMediaSource(dataSource, stream, cacheKey, metadata);
case SS:
return buildSSMediaSource(dataSource, stream, cacheKey, metadata);
// Torrent streams are not supported by ExoPlayer
default:
throw new 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.net.Uri;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -27,8 +28,12 @@ import java.util.List;
import java.util.Optional;
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 {
private static final String TAG = VideoPlaybackResolver.class.getSimpleName();
@NonNull
private final Context context;
@NonNull
@ -57,7 +62,7 @@ public class VideoPlaybackResolver implements PlaybackResolver {
@Override
@Nullable
public MediaSource resolve(@NonNull final StreamInfo info) {
final MediaSource liveSource = maybeBuildLiveMediaSource(dataSource, info);
final MediaSource liveSource = PlaybackResolver.maybeBuildLiveMediaSource(dataSource, info);
if (liveSource != null) {
streamSourceType = SourceType.LIVE_STREAM;
return liveSource;
@ -66,40 +71,51 @@ public class VideoPlaybackResolver implements PlaybackResolver {
final List<MediaSource> mediaSources = new ArrayList<>();
// Create video stream source
final List<VideoStream> videos = ListHelper.getSortedStreamVideosList(context,
info.getVideoStreams(), info.getVideoOnlyStreams(), false, true);
final List<VideoStream> videoStreamsList = ListHelper.getSortedStreamVideosList(context,
getNonTorrentStreams(info.getVideoStreams()),
getNonTorrentStreams(info.getVideoOnlyStreams()), false, true);
final int index;
if (videos.isEmpty()) {
if (videoStreamsList.isEmpty()) {
index = -1;
} else if (playbackQuality == null) {
index = qualityResolver.getDefaultResolutionIndex(videos);
index = qualityResolver.getDefaultResolutionIndex(videoStreamsList);
} 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()
.map(MediaItemTag.Quality::getSelectedVideoStream)
.orElse(null);
if (video != null) {
final MediaSource streamSource = buildMediaSource(dataSource, video.getUrl(),
PlayerHelper.cacheKeyOf(info, video),
MediaFormat.getSuffixById(video.getFormatId()), tag);
mediaSources.add(streamSource);
try {
final MediaSource streamSource = PlaybackResolver.buildMediaSource(
dataSource, video, info, PlaybackResolver.cacheKeyOf(info, video), tag);
mediaSources.add(streamSource);
} catch (final ResolverException e) {
Log.e(TAG, "Unable to create video source", e);
return null;
}
}
// 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(
ListHelper.getDefaultAudioFormat(context, audioStreams));
// Use the audio stream if there is no video stream, or
// Merge with audio stream in case if video does not contain audio
if (audio != null && (video == null || video.isVideoOnly)) {
final MediaSource audioSource = buildMediaSource(dataSource, audio.getUrl(),
PlayerHelper.cacheKeyOf(info, audio),
MediaFormat.getSuffixById(audio.getFormatId()), tag);
mediaSources.add(audioSource);
streamSourceType = SourceType.VIDEO_WITH_SEPARATED_AUDIO;
// merge with audio stream in case if video does not contain audio
if (audio != null && (video == null || video.isVideoOnly())) {
try {
final MediaSource audioSource = PlaybackResolver.buildMediaSource(
dataSource, audio, info, PlaybackResolver.cacheKeyOf(info, audio), tag);
mediaSources.add(audioSource);
streamSourceType = SourceType.VIDEO_WITH_SEPARATED_AUDIO;
} catch (final ResolverException e) {
Log.e(TAG, "Unable to create audio source", e);
return null;
}
} else {
streamSourceType = SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY;
}
@ -108,36 +124,39 @@ public class VideoPlaybackResolver implements PlaybackResolver {
if (mediaSources.isEmpty()) {
return null;
}
// Below are auxiliary media sources
// Create subtitle sources
if (info.getSubtitles() != null) {
for (final SubtitlesStream subtitle : info.getSubtitles()) {
final String mimeType = PlayerHelper.subtitleMimeTypesOf(subtitle.getFormat());
if (mimeType == null) {
continue;
final List<SubtitlesStream> subtitlesStreams = info.getSubtitles();
if (subtitlesStreams != null) {
// Torrent and non URL subtitles are not supported by ExoPlayer
final List<SubtitlesStream> nonTorrentAndUrlStreams = getUrlAndNonTorrentStreams(
subtitlesStreams);
for (final SubtitlesStream subtitle : nonTorrentAndUrlStreams) {
final MediaFormat mediaFormat = subtitle.getFormat();
if (mediaFormat != null) {
@C.RoleFlags final int textRoleFlag = subtitle.isAutoGenerated()
? C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND
: C.ROLE_FLAG_CAPTION;
final MediaItem.SubtitleConfiguration textMediaItem =
new MediaItem.SubtitleConfiguration.Builder(
Uri.parse(subtitle.getContent()))
.setMimeType(mediaFormat.getMimeType())
.setRoleFlags(textRoleFlag)
.setLanguage(PlayerHelper.captionLanguageOf(context, subtitle))
.build();
final MediaSource textSource = dataSource.getSingleSampleMediaSourceFactory()
.createMediaSource(textMediaItem, TIME_UNSET);
mediaSources.add(textSource);
}
final @C.RoleFlags int textRoleFlag = subtitle.isAutoGenerated()
? C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND
: C.ROLE_FLAG_CAPTION;
final MediaItem.SubtitleConfiguration textMediaItem =
new MediaItem.SubtitleConfiguration.Builder(Uri.parse(subtitle.getUrl()))
.setMimeType(mimeType)
.setRoleFlags(textRoleFlag)
.setLanguage(PlayerHelper.captionLanguageOf(context, subtitle))
.build();
final MediaSource textSource = dataSource
.getSampleMediaSourceFactory()
.createMediaSource(textMediaItem, TIME_UNSET);
mediaSources.add(textSource);
}
}
if (mediaSources.size() == 1) {
return mediaSources.get(0);
} else {
return new MergingMediaSource(mediaSources.toArray(
new MediaSource[0]));
return new MergingMediaSource(true, mediaSources.toArray(new MediaSource[0]));
}
}

View file

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

View file

@ -33,6 +33,7 @@ import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.DeliveryMethod;
import org.schabi.newpipe.extractor.stream.Stream;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
@ -60,7 +61,9 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.settings.SettingsActivity;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import java.util.ArrayList;
import java.util.List;
import static org.schabi.newpipe.util.ListHelper.getUrlAndNonTorrentStreams;
public final class NavigationHelper {
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,
@NonNull final StreamInfo info) {
final int index = ListHelper.getDefaultAudioFormat(context, info.getAudioStreams());
if (index == -1) {
final List<AudioStream> audioStreams = info.getAudioStreams();
if (audioStreams == null || audioStreams.isEmpty()) {
Toast.makeText(context, R.string.audio_streams_empty, Toast.LENGTH_SHORT).show();
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);
}
public static void playOnExternalVideoPlayer(@NonNull final Context context,
public static void playOnExternalVideoPlayer(final Context context,
@NonNull final StreamInfo info) {
final ArrayList<VideoStream> videoStreamsList = new ArrayList<>(
ListHelper.getSortedStreamVideosList(context, info.getVideoStreams(), null, false,
false));
final int index = ListHelper.getDefaultResolutionIndex(context, videoStreamsList);
if (index == -1) {
final List<VideoStream> videoStreams = info.getVideoStreams();
if (videoStreams == null || videoStreams.isEmpty()) {
Toast.makeText(context, R.string.video_streams_empty, Toast.LENGTH_SHORT).show();
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);
}
@ -248,9 +268,48 @@ public final class NavigationHelper {
@Nullable final String name,
@Nullable final String artist,
@NonNull final Stream stream) {
final DeliveryMethod deliveryMethod = stream.getDeliveryMethod();
final String mimeType;
if (!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();
intent.setAction(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.parse(stream.getUrl()), stream.getFormat().getMimeType());
intent.setDataAndType(Uri.parse(stream.getContent()), mimeType);
intent.putExtra(Intent.EXTRA_TITLE, name);
intent.putExtra("title", name);
intent.putExtra("artist", artist);

View file

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

View file

@ -1,7 +1,5 @@
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 android.content.Context;
@ -49,8 +47,8 @@ public final class SparseItemUtil {
public static void fetchItemInfoIfSparse(@NonNull final Context context,
@NonNull final StreamInfoItem item,
@NonNull final Consumer<SinglePlayQueue> callback) {
if (((item.getStreamType() == LIVE_STREAM || item.getStreamType() == AUDIO_LIVE_STREAM)
|| item.getDuration() >= 0) && !isNullOrEmpty(item.getUploaderUrl())) {
if ((StreamTypeUtil.isLiveStream(item.getStreamType()) || item.getDuration() >= 0)
&& !isNullOrEmpty(item.getUploaderUrl())) {
// 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
callback.accept(new SinglePlayQueue(item));

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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