Merge pull request #8153 from AudricV/delivery-methods-v2
Support delivery methods other than progressive HTTP
This commit is contained in:
commit
3901ffca17
30 changed files with 2537 additions and 632 deletions
|
@ -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}"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)};
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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]));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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('}')
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue