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
|
// name and the commit hash with the commit hash of the (pushed) commit you want to test
|
||||||
// This works thanks to JitPack: https://jitpack.io/
|
// This works thanks to JitPack: https://jitpack.io/
|
||||||
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
|
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
|
||||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:ac1c22d81c65b7b0c5427f4e1989f5256d617f32'
|
implementation 'com.github.TeamNewPipe:NewPipeExtractor:1b51eab664ec7cbd2295c96d8b43000379cd1b7b'
|
||||||
|
|
||||||
/** Checkstyle **/
|
/** Checkstyle **/
|
||||||
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
|
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
|
||||||
|
|
|
@ -91,7 +91,12 @@ class StreamItemAdapterTest {
|
||||||
context,
|
context,
|
||||||
StreamItemAdapter.StreamSizeWrapper(
|
StreamItemAdapter.StreamSizeWrapper(
|
||||||
(0 until 5).map {
|
(0 until 5).map {
|
||||||
SubtitlesStream(MediaFormat.SRT, "pt-BR", "https://example.com", false)
|
SubtitlesStream.Builder()
|
||||||
|
.setContent("https://example.com", true)
|
||||||
|
.setMediaFormat(MediaFormat.SRT)
|
||||||
|
.setLanguageCode("pt-BR")
|
||||||
|
.setAutoGenerated(false)
|
||||||
|
.build()
|
||||||
},
|
},
|
||||||
context
|
context
|
||||||
),
|
),
|
||||||
|
@ -108,7 +113,14 @@ class StreamItemAdapterTest {
|
||||||
val adapter = StreamItemAdapter<AudioStream, Stream>(
|
val adapter = StreamItemAdapter<AudioStream, Stream>(
|
||||||
context,
|
context,
|
||||||
StreamItemAdapter.StreamSizeWrapper(
|
StreamItemAdapter.StreamSizeWrapper(
|
||||||
(0 until 5).map { AudioStream("https://example.com/$it", MediaFormat.OPUS, 192) },
|
(0 until 5).map {
|
||||||
|
AudioStream.Builder()
|
||||||
|
.setId(Stream.ID_UNKNOWN)
|
||||||
|
.setContent("https://example.com/$it", true)
|
||||||
|
.setMediaFormat(MediaFormat.OPUS)
|
||||||
|
.setAverageBitrate(192)
|
||||||
|
.build()
|
||||||
|
},
|
||||||
context
|
context
|
||||||
),
|
),
|
||||||
null
|
null
|
||||||
|
@ -126,7 +138,13 @@ class StreamItemAdapterTest {
|
||||||
private fun getVideoStreams(vararg videoOnly: Boolean) =
|
private fun getVideoStreams(vararg videoOnly: Boolean) =
|
||||||
StreamItemAdapter.StreamSizeWrapper(
|
StreamItemAdapter.StreamSizeWrapper(
|
||||||
videoOnly.map {
|
videoOnly.map {
|
||||||
VideoStream("https://example.com", MediaFormat.MPEG_4, "720p", it)
|
VideoStream.Builder()
|
||||||
|
.setId(Stream.ID_UNKNOWN)
|
||||||
|
.setContent("https://example.com", true)
|
||||||
|
.setMediaFormat(MediaFormat.MPEG_4)
|
||||||
|
.setResolution("720p")
|
||||||
|
.setIsVideoOnly(it)
|
||||||
|
.build()
|
||||||
},
|
},
|
||||||
context
|
context
|
||||||
)
|
)
|
||||||
|
@ -138,8 +156,16 @@ class StreamItemAdapterTest {
|
||||||
private fun getAudioStreams(vararg shouldBeValid: Boolean) =
|
private fun getAudioStreams(vararg shouldBeValid: Boolean) =
|
||||||
getSecondaryStreamsFromList(
|
getSecondaryStreamsFromList(
|
||||||
shouldBeValid.map {
|
shouldBeValid.map {
|
||||||
if (it) AudioStream("https://example.com", MediaFormat.OPUS, 192)
|
if (it) {
|
||||||
else null
|
AudioStream.Builder()
|
||||||
|
.setId(Stream.ID_UNKNOWN)
|
||||||
|
.setContent("https://example.com", true)
|
||||||
|
.setMediaFormat(MediaFormat.OPUS)
|
||||||
|
.setAverageBitrate(192)
|
||||||
|
.build()
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -58,7 +58,6 @@ import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException;
|
import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException;
|
||||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
|
||||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
import org.schabi.newpipe.ktx.ExceptionUtils;
|
||||||
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||||
import org.schabi.newpipe.player.MainPlayer;
|
import org.schabi.newpipe.player.MainPlayer;
|
||||||
|
@ -71,7 +70,6 @@ import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||||
import org.schabi.newpipe.util.Constants;
|
import org.schabi.newpipe.util.Constants;
|
||||||
import org.schabi.newpipe.util.DeviceUtils;
|
import org.schabi.newpipe.util.DeviceUtils;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
import org.schabi.newpipe.util.ListHelper;
|
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.PermissionHelper;
|
import org.schabi.newpipe.util.PermissionHelper;
|
||||||
|
@ -677,22 +675,13 @@ public class RouterActivity extends AppCompatActivity {
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(result -> {
|
.subscribe(result -> {
|
||||||
final List<VideoStream> sortedVideoStreams = ListHelper
|
final DownloadDialog downloadDialog = new DownloadDialog(this, result);
|
||||||
.getSortedStreamVideosList(this, result.getVideoStreams(),
|
downloadDialog.setOnDismissListener(dialog -> finish());
|
||||||
result.getVideoOnlyStreams(), false, false);
|
|
||||||
final int selectedVideoStreamIndex = ListHelper
|
|
||||||
.getDefaultResolutionIndex(this, sortedVideoStreams);
|
|
||||||
|
|
||||||
final FragmentManager fm = getSupportFragmentManager();
|
final FragmentManager fm = getSupportFragmentManager();
|
||||||
final DownloadDialog downloadDialog = DownloadDialog.newInstance(result);
|
|
||||||
downloadDialog.setVideoStreams(sortedVideoStreams);
|
|
||||||
downloadDialog.setAudioStreams(result.getAudioStreams());
|
|
||||||
downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex);
|
|
||||||
downloadDialog.setOnDismissListener(dialog -> finish());
|
|
||||||
downloadDialog.show(fm, "downloadDialog");
|
downloadDialog.show(fm, "downloadDialog");
|
||||||
fm.executePendingTransactions();
|
fm.executePendingTransactions();
|
||||||
}, throwable ->
|
}, throwable -> showUnsupportedUrlDialog(currentUrl)));
|
||||||
showUnsupportedUrlDialog(currentUrl)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -12,8 +12,7 @@ import org.schabi.newpipe.database.BasicDAO
|
||||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||||
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_ID
|
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_ID
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType
|
import org.schabi.newpipe.extractor.stream.StreamType
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_LIVE_STREAM
|
import org.schabi.newpipe.util.StreamTypeUtil
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType.LIVE_STREAM
|
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
|
@ -91,8 +90,7 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
|
||||||
?: throw IllegalStateException("Stream cannot be null just after insertion.")
|
?: throw IllegalStateException("Stream cannot be null just after insertion.")
|
||||||
newerStream.uid = existentMinimalStream.uid
|
newerStream.uid = existentMinimalStream.uid
|
||||||
|
|
||||||
val isNewerStreamLive = newerStream.streamType == AUDIO_LIVE_STREAM || newerStream.streamType == LIVE_STREAM
|
if (!StreamTypeUtil.isLiveStream(newerStream.streamType)) {
|
||||||
if (!isNewerStreamLive) {
|
|
||||||
|
|
||||||
// Use the existent upload date if the newer stream does not have a better precision
|
// Use the existent upload date if the newer stream does not have a better precision
|
||||||
// (i.e. is an approximation). This is done to prevent unnecessary changes.
|
// (i.e. is an approximation). This is done to prevent unnecessary changes.
|
||||||
|
|
|
@ -68,9 +68,9 @@ import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
import icepick.Icepick;
|
import icepick.Icepick;
|
||||||
import icepick.State;
|
import icepick.State;
|
||||||
|
@ -82,6 +82,8 @@ import us.shandian.giga.service.DownloadManagerService;
|
||||||
import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder;
|
import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder;
|
||||||
import us.shandian.giga.service.MissionState;
|
import us.shandian.giga.service.MissionState;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.extractor.stream.DeliveryMethod.PROGRESSIVE_HTTP;
|
||||||
|
import static org.schabi.newpipe.util.ListHelper.getStreamsOfSpecifiedDelivery;
|
||||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||||
|
|
||||||
public class DownloadDialog extends DialogFragment
|
public class DownloadDialog extends DialogFragment
|
||||||
|
@ -92,17 +94,17 @@ public class DownloadDialog extends DialogFragment
|
||||||
@State
|
@State
|
||||||
StreamInfo currentInfo;
|
StreamInfo currentInfo;
|
||||||
@State
|
@State
|
||||||
StreamSizeWrapper<AudioStream> wrappedAudioStreams = StreamSizeWrapper.empty();
|
StreamSizeWrapper<AudioStream> wrappedAudioStreams;
|
||||||
@State
|
@State
|
||||||
StreamSizeWrapper<VideoStream> wrappedVideoStreams = StreamSizeWrapper.empty();
|
StreamSizeWrapper<VideoStream> wrappedVideoStreams;
|
||||||
@State
|
@State
|
||||||
StreamSizeWrapper<SubtitlesStream> wrappedSubtitleStreams = StreamSizeWrapper.empty();
|
StreamSizeWrapper<SubtitlesStream> wrappedSubtitleStreams;
|
||||||
@State
|
@State
|
||||||
int selectedVideoIndex = 0;
|
int selectedVideoIndex; // set in the constructor
|
||||||
@State
|
@State
|
||||||
int selectedAudioIndex = 0;
|
int selectedAudioIndex = 0; // default to the first item
|
||||||
@State
|
@State
|
||||||
int selectedSubtitleIndex = 0;
|
int selectedSubtitleIndex = 0; // default to the first item
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private OnDismissListener onDismissListener = null;
|
private OnDismissListener onDismissListener = null;
|
||||||
|
@ -143,77 +145,43 @@ public class DownloadDialog extends DialogFragment
|
||||||
// Instance creation
|
// Instance creation
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
public static DownloadDialog newInstance(final StreamInfo info) {
|
/**
|
||||||
final DownloadDialog dialog = new DownloadDialog();
|
* Create a new download dialog with the video, audio and subtitle streams from the provided
|
||||||
dialog.setInfo(info);
|
* stream info. Video streams and video-only streams will be put into a single list menu,
|
||||||
return dialog;
|
* sorted according to their resolution and the default video resolution will be selected.
|
||||||
}
|
*
|
||||||
|
* @param context the context to use just to obtain preferences and strings (will not be stored)
|
||||||
public static DownloadDialog newInstance(final Context context, final StreamInfo info) {
|
* @param info the info from which to obtain downloadable streams and other info (e.g. title)
|
||||||
final ArrayList<VideoStream> streamsList = new ArrayList<>(ListHelper
|
*/
|
||||||
.getSortedStreamVideosList(context, info.getVideoStreams(),
|
public DownloadDialog(final Context context, @NonNull final StreamInfo info) {
|
||||||
info.getVideoOnlyStreams(), false, false));
|
|
||||||
final int selectedStreamIndex = ListHelper.getDefaultResolutionIndex(context, streamsList);
|
|
||||||
|
|
||||||
final DownloadDialog instance = newInstance(info);
|
|
||||||
instance.setVideoStreams(streamsList);
|
|
||||||
instance.setSelectedVideoStream(selectedStreamIndex);
|
|
||||||
instance.setAudioStreams(info.getAudioStreams());
|
|
||||||
instance.setSubtitleStreams(info.getSubtitles());
|
|
||||||
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Setters
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
private void setInfo(final StreamInfo info) {
|
|
||||||
this.currentInfo = info;
|
this.currentInfo = info;
|
||||||
|
|
||||||
|
// TODO: Adapt this code when the downloader support other types of stream deliveries
|
||||||
|
final List<VideoStream> videoStreams = ListHelper.getSortedStreamVideosList(
|
||||||
|
context,
|
||||||
|
getStreamsOfSpecifiedDelivery(info.getVideoStreams(), PROGRESSIVE_HTTP),
|
||||||
|
getStreamsOfSpecifiedDelivery(info.getVideoOnlyStreams(), PROGRESSIVE_HTTP),
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
this.wrappedVideoStreams = new StreamSizeWrapper<>(videoStreams, context);
|
||||||
|
this.wrappedAudioStreams = new StreamSizeWrapper<>(
|
||||||
|
getStreamsOfSpecifiedDelivery(info.getAudioStreams(), PROGRESSIVE_HTTP), context);
|
||||||
|
this.wrappedSubtitleStreams = new StreamSizeWrapper<>(
|
||||||
|
getStreamsOfSpecifiedDelivery(info.getSubtitles(), PROGRESSIVE_HTTP), context);
|
||||||
|
|
||||||
|
this.selectedVideoIndex = ListHelper.getDefaultResolutionIndex(context, videoStreams);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setAudioStreams(final List<AudioStream> audioStreams) {
|
/**
|
||||||
setAudioStreams(new StreamSizeWrapper<>(audioStreams, getContext()));
|
* @param onDismissListener the listener to call in {@link #onDismiss(DialogInterface)}
|
||||||
}
|
*/
|
||||||
|
|
||||||
public void setAudioStreams(final StreamSizeWrapper<AudioStream> was) {
|
|
||||||
this.wrappedAudioStreams = was;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setVideoStreams(final List<VideoStream> videoStreams) {
|
|
||||||
setVideoStreams(new StreamSizeWrapper<>(videoStreams, getContext()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setVideoStreams(final StreamSizeWrapper<VideoStream> wvs) {
|
|
||||||
this.wrappedVideoStreams = wvs;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSubtitleStreams(final List<SubtitlesStream> subtitleStreams) {
|
|
||||||
setSubtitleStreams(new StreamSizeWrapper<>(subtitleStreams, getContext()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSubtitleStreams(
|
|
||||||
final StreamSizeWrapper<SubtitlesStream> wss) {
|
|
||||||
this.wrappedSubtitleStreams = wss;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSelectedVideoStream(final int svi) {
|
|
||||||
this.selectedVideoIndex = svi;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSelectedAudioStream(final int sai) {
|
|
||||||
this.selectedAudioIndex = sai;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSelectedSubtitleStream(final int ssi) {
|
|
||||||
this.selectedSubtitleIndex = ssi;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOnDismissListener(@Nullable final OnDismissListener onDismissListener) {
|
public void setOnDismissListener(@Nullable final OnDismissListener onDismissListener) {
|
||||||
this.onDismissListener = onDismissListener;
|
this.onDismissListener = onDismissListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Android lifecycle
|
// Android lifecycle
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
@ -249,11 +217,16 @@ public class DownloadDialog extends DialogFragment
|
||||||
.getAudioStreamFor(wrappedAudioStreams.getStreamsList(), videoStreams.get(i));
|
.getAudioStreamFor(wrappedAudioStreams.getStreamsList(), videoStreams.get(i));
|
||||||
|
|
||||||
if (audioStream != null) {
|
if (audioStream != null) {
|
||||||
secondaryStreams
|
secondaryStreams.append(i, new SecondaryStreamHelper<>(wrappedAudioStreams,
|
||||||
.append(i, new SecondaryStreamHelper<>(wrappedAudioStreams, audioStream));
|
audioStream));
|
||||||
} else if (DEBUG) {
|
} else if (DEBUG) {
|
||||||
Log.w(TAG, "No audio stream candidates for video format "
|
final MediaFormat mediaFormat = videoStreams.get(i).getFormat();
|
||||||
+ videoStreams.get(i).getFormat().name());
|
if (mediaFormat != null) {
|
||||||
|
Log.w(TAG, "No audio stream candidates for video format "
|
||||||
|
+ mediaFormat.name());
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "No audio stream candidates for unknown video format");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -288,7 +261,8 @@ public class DownloadDialog extends DialogFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container,
|
public View onCreateView(@NonNull final LayoutInflater inflater,
|
||||||
|
final ViewGroup container,
|
||||||
final Bundle savedInstanceState) {
|
final Bundle savedInstanceState) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "onCreateView() called with: "
|
Log.d(TAG, "onCreateView() called with: "
|
||||||
|
@ -299,14 +273,15 @@ public class DownloadDialog extends DialogFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
|
public void onViewCreated(@NonNull final View view,
|
||||||
|
@Nullable final Bundle savedInstanceState) {
|
||||||
super.onViewCreated(view, savedInstanceState);
|
super.onViewCreated(view, savedInstanceState);
|
||||||
dialogBinding = DownloadDialogBinding.bind(view);
|
dialogBinding = DownloadDialogBinding.bind(view);
|
||||||
|
|
||||||
dialogBinding.fileName.setText(FilenameUtils.createFilename(getContext(),
|
dialogBinding.fileName.setText(FilenameUtils.createFilename(getContext(),
|
||||||
currentInfo.getName()));
|
currentInfo.getName()));
|
||||||
selectedAudioIndex = ListHelper
|
selectedAudioIndex = ListHelper
|
||||||
.getDefaultAudioFormat(getContext(), currentInfo.getAudioStreams());
|
.getDefaultAudioFormat(getContext(), wrappedAudioStreams.getStreamsList());
|
||||||
|
|
||||||
selectedSubtitleIndex = getSubtitleIndexBy(subtitleStreamsAdapter.getAll());
|
selectedSubtitleIndex = getSubtitleIndexBy(subtitleStreamsAdapter.getAll());
|
||||||
|
|
||||||
|
@ -324,7 +299,8 @@ public class DownloadDialog extends DialogFragment
|
||||||
dialogBinding.threads.setProgress(threads - 1);
|
dialogBinding.threads.setProgress(threads - 1);
|
||||||
dialogBinding.threads.setOnSeekBarChangeListener(new SimpleOnSeekBarChangeListener() {
|
dialogBinding.threads.setOnSeekBarChangeListener(new SimpleOnSeekBarChangeListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onProgressChanged(@NonNull final SeekBar seekbar, final int progress,
|
public void onProgressChanged(@NonNull final SeekBar seekbar,
|
||||||
|
final int progress,
|
||||||
final boolean fromUser) {
|
final boolean fromUser) {
|
||||||
final int newProgress = progress + 1;
|
final int newProgress = progress + 1;
|
||||||
prefs.edit().putInt(getString(R.string.default_download_threads), newProgress)
|
prefs.edit().putInt(getString(R.string.default_download_threads), newProgress)
|
||||||
|
@ -469,7 +445,7 @@ public class DownloadDialog extends DialogFragment
|
||||||
result, getString(R.string.download_path_video_key), DownloadManager.TAG_VIDEO);
|
result, getString(R.string.download_path_video_key), DownloadManager.TAG_VIDEO);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void requestDownloadSaveAsResult(final ActivityResult result) {
|
private void requestDownloadSaveAsResult(@NonNull final ActivityResult result) {
|
||||||
if (result.getResultCode() != Activity.RESULT_OK) {
|
if (result.getResultCode() != Activity.RESULT_OK) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -486,8 +462,8 @@ public class DownloadDialog extends DialogFragment
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final DocumentFile docFile
|
final DocumentFile docFile = DocumentFile.fromSingleUri(context,
|
||||||
= DocumentFile.fromSingleUri(context, result.getData().getData());
|
result.getData().getData());
|
||||||
if (docFile == null) {
|
if (docFile == null) {
|
||||||
showFailedDialog(R.string.general_error);
|
showFailedDialog(R.string.general_error);
|
||||||
return;
|
return;
|
||||||
|
@ -498,7 +474,7 @@ public class DownloadDialog extends DialogFragment
|
||||||
docFile.getType());
|
docFile.getType());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void requestDownloadPickFolderResult(final ActivityResult result,
|
private void requestDownloadPickFolderResult(@NonNull final ActivityResult result,
|
||||||
final String key,
|
final String key,
|
||||||
final String tag) {
|
final String tag) {
|
||||||
if (result.getResultCode() != Activity.RESULT_OK) {
|
if (result.getResultCode() != Activity.RESULT_OK) {
|
||||||
|
@ -518,12 +494,11 @@ public class DownloadDialog extends DialogFragment
|
||||||
StoredDirectoryHelper.PERMISSION_FLAGS);
|
StoredDirectoryHelper.PERMISSION_FLAGS);
|
||||||
}
|
}
|
||||||
|
|
||||||
PreferenceManager.getDefaultSharedPreferences(context).edit()
|
PreferenceManager.getDefaultSharedPreferences(context).edit().putString(key,
|
||||||
.putString(key, uri.toString()).apply();
|
uri.toString()).apply();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final StoredDirectoryHelper mainStorage
|
final StoredDirectoryHelper mainStorage = new StoredDirectoryHelper(context, uri, tag);
|
||||||
= new StoredDirectoryHelper(context, uri, tag);
|
|
||||||
checkSelectedDownload(mainStorage, mainStorage.findFile(filenameTmp),
|
checkSelectedDownload(mainStorage, mainStorage.findFile(filenameTmp),
|
||||||
filenameTmp, mimeTmp);
|
filenameTmp, mimeTmp);
|
||||||
} catch (final IOException e) {
|
} catch (final IOException e) {
|
||||||
|
@ -561,8 +536,10 @@ public class DownloadDialog extends DialogFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onItemSelected(final AdapterView<?> parent, final View view,
|
public void onItemSelected(final AdapterView<?> parent,
|
||||||
final int position, final long id) {
|
final View view,
|
||||||
|
final int position,
|
||||||
|
final long id) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "onItemSelected() called with: "
|
Log.d(TAG, "onItemSelected() called with: "
|
||||||
+ "parent = [" + parent + "], view = [" + view + "], "
|
+ "parent = [" + parent + "], view = [" + view + "], "
|
||||||
|
@ -597,14 +574,16 @@ public class DownloadDialog extends DialogFragment
|
||||||
final boolean isAudioStreamsAvailable = audioStreamsAdapter.getCount() > 0;
|
final boolean isAudioStreamsAvailable = audioStreamsAdapter.getCount() > 0;
|
||||||
final boolean isSubtitleStreamsAvailable = subtitleStreamsAdapter.getCount() > 0;
|
final boolean isSubtitleStreamsAvailable = subtitleStreamsAdapter.getCount() > 0;
|
||||||
|
|
||||||
dialogBinding.audioButton.setVisibility(isAudioStreamsAvailable ? View.VISIBLE : View.GONE);
|
dialogBinding.audioButton.setVisibility(isAudioStreamsAvailable ? View.VISIBLE
|
||||||
dialogBinding.videoButton.setVisibility(isVideoStreamsAvailable ? View.VISIBLE : View.GONE);
|
: View.GONE);
|
||||||
|
dialogBinding.videoButton.setVisibility(isVideoStreamsAvailable ? View.VISIBLE
|
||||||
|
: View.GONE);
|
||||||
dialogBinding.subtitleButton.setVisibility(isSubtitleStreamsAvailable
|
dialogBinding.subtitleButton.setVisibility(isSubtitleStreamsAvailable
|
||||||
? View.VISIBLE : View.GONE);
|
? View.VISIBLE : View.GONE);
|
||||||
|
|
||||||
prefs = PreferenceManager.getDefaultSharedPreferences(requireContext());
|
prefs = PreferenceManager.getDefaultSharedPreferences(requireContext());
|
||||||
final String defaultMedia = prefs.getString(getString(R.string.last_used_download_type),
|
final String defaultMedia = prefs.getString(getString(R.string.last_used_download_type),
|
||||||
getString(R.string.last_download_type_video_key));
|
getString(R.string.last_download_type_video_key));
|
||||||
|
|
||||||
if (isVideoStreamsAvailable
|
if (isVideoStreamsAvailable
|
||||||
&& (defaultMedia.equals(getString(R.string.last_download_type_video_key)))) {
|
&& (defaultMedia.equals(getString(R.string.last_download_type_video_key)))) {
|
||||||
|
@ -640,7 +619,7 @@ public class DownloadDialog extends DialogFragment
|
||||||
dialogBinding.subtitleButton.setEnabled(enabled);
|
dialogBinding.subtitleButton.setEnabled(enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getSubtitleIndexBy(final List<SubtitlesStream> streams) {
|
private int getSubtitleIndexBy(@NonNull final List<SubtitlesStream> streams) {
|
||||||
final Localization preferredLocalization = NewPipe.getPreferredLocalization();
|
final Localization preferredLocalization = NewPipe.getPreferredLocalization();
|
||||||
|
|
||||||
int candidate = 0;
|
int candidate = 0;
|
||||||
|
@ -666,8 +645,10 @@ public class DownloadDialog extends DialogFragment
|
||||||
return candidate;
|
return candidate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
private String getNameEditText() {
|
private String getNameEditText() {
|
||||||
final String str = dialogBinding.fileName.getText().toString().trim();
|
final String str = Objects.requireNonNull(dialogBinding.fileName.getText()).toString()
|
||||||
|
.trim();
|
||||||
|
|
||||||
return FilenameUtils.createFilename(context, str.isEmpty() ? currentInfo.getName() : str);
|
return FilenameUtils.createFilename(context, str.isEmpty() ? currentInfo.getName() : str);
|
||||||
}
|
}
|
||||||
|
@ -683,12 +664,8 @@ public class DownloadDialog extends DialogFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
private void launchDirectoryPicker(final ActivityResultLauncher<Intent> launcher) {
|
private void launchDirectoryPicker(final ActivityResultLauncher<Intent> launcher) {
|
||||||
NoFileManagerSafeGuard.launchSafe(
|
NoFileManagerSafeGuard.launchSafe(launcher, StoredDirectoryHelper.getPicker(context), TAG,
|
||||||
launcher,
|
context);
|
||||||
StoredDirectoryHelper.getPicker(context),
|
|
||||||
TAG,
|
|
||||||
context
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void prepareSelectedDownload() {
|
private void prepareSelectedDownload() {
|
||||||
|
@ -709,7 +686,7 @@ public class DownloadDialog extends DialogFragment
|
||||||
if (format == MediaFormat.WEBMA_OPUS) {
|
if (format == MediaFormat.WEBMA_OPUS) {
|
||||||
mimeTmp = "audio/ogg";
|
mimeTmp = "audio/ogg";
|
||||||
filenameTmp += "opus";
|
filenameTmp += "opus";
|
||||||
} else {
|
} else if (format != null) {
|
||||||
mimeTmp = format.mimeType;
|
mimeTmp = format.mimeType;
|
||||||
filenameTmp += format.suffix;
|
filenameTmp += format.suffix;
|
||||||
}
|
}
|
||||||
|
@ -718,22 +695,30 @@ public class DownloadDialog extends DialogFragment
|
||||||
selectedMediaType = getString(R.string.last_download_type_video_key);
|
selectedMediaType = getString(R.string.last_download_type_video_key);
|
||||||
mainStorage = mainStorageVideo;
|
mainStorage = mainStorageVideo;
|
||||||
format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
|
format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
|
||||||
mimeTmp = format.mimeType;
|
if (format != null) {
|
||||||
filenameTmp += format.suffix;
|
mimeTmp = format.mimeType;
|
||||||
|
filenameTmp += format.suffix;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case R.id.subtitle_button:
|
case R.id.subtitle_button:
|
||||||
selectedMediaType = getString(R.string.last_download_type_subtitle_key);
|
selectedMediaType = getString(R.string.last_download_type_subtitle_key);
|
||||||
mainStorage = mainStorageVideo; // subtitle & video files go together
|
mainStorage = mainStorageVideo; // subtitle & video files go together
|
||||||
format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat();
|
format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat();
|
||||||
mimeTmp = format.mimeType;
|
if (format != null) {
|
||||||
filenameTmp += (format == MediaFormat.TTML ? MediaFormat.SRT : format).suffix;
|
mimeTmp = format.mimeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (format == MediaFormat.TTML) {
|
||||||
|
filenameTmp += MediaFormat.SRT.suffix;
|
||||||
|
} else if (format != null) {
|
||||||
|
filenameTmp += format.suffix;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new RuntimeException("No stream selected");
|
throw new RuntimeException("No stream selected");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!askForSavePath
|
if (!askForSavePath && (mainStorage == null
|
||||||
&& (mainStorage == null
|
|
||||||
|| mainStorage.isDirect() == NewPipeSettings.useStorageAccessFramework(context)
|
|| mainStorage.isDirect() == NewPipeSettings.useStorageAccessFramework(context)
|
||||||
|| mainStorage.isInvalidSafStorage())) {
|
|| mainStorage.isInvalidSafStorage())) {
|
||||||
// Pick new download folder if one of:
|
// Pick new download folder if one of:
|
||||||
|
@ -767,18 +752,16 @@ public class DownloadDialog extends DialogFragment
|
||||||
initialPath = Uri.parse(initialSavePath.getAbsolutePath());
|
initialPath = Uri.parse(initialSavePath.getAbsolutePath());
|
||||||
}
|
}
|
||||||
|
|
||||||
NoFileManagerSafeGuard.launchSafe(
|
NoFileManagerSafeGuard.launchSafe(requestDownloadSaveAsLauncher,
|
||||||
requestDownloadSaveAsLauncher,
|
StoredFileHelper.getNewPicker(context, filenameTmp, mimeTmp, initialPath), TAG,
|
||||||
StoredFileHelper.getNewPicker(context, filenameTmp, mimeTmp, initialPath),
|
context);
|
||||||
TAG,
|
|
||||||
context
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// check for existing file with the same name
|
// check for existing file with the same name
|
||||||
checkSelectedDownload(mainStorage, mainStorage.findFile(filenameTmp), filenameTmp, mimeTmp);
|
checkSelectedDownload(mainStorage, mainStorage.findFile(filenameTmp), filenameTmp,
|
||||||
|
mimeTmp);
|
||||||
|
|
||||||
// remember the last media type downloaded by the user
|
// remember the last media type downloaded by the user
|
||||||
prefs.edit().putString(getString(R.string.last_used_download_type), selectedMediaType)
|
prefs.edit().putString(getString(R.string.last_used_download_type), selectedMediaType)
|
||||||
|
@ -786,7 +769,8 @@ public class DownloadDialog extends DialogFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkSelectedDownload(final StoredDirectoryHelper mainStorage,
|
private void checkSelectedDownload(final StoredDirectoryHelper mainStorage,
|
||||||
final Uri targetFile, final String filename,
|
final Uri targetFile,
|
||||||
|
final String filename,
|
||||||
final String mime) {
|
final String mime) {
|
||||||
StoredFileHelper storage;
|
StoredFileHelper storage;
|
||||||
|
|
||||||
|
@ -947,7 +931,7 @@ public class DownloadDialog extends DialogFragment
|
||||||
storage.truncate();
|
storage.truncate();
|
||||||
}
|
}
|
||||||
} catch (final IOException e) {
|
} catch (final IOException e) {
|
||||||
Log.e(TAG, "failed to truncate the file: " + storage.getUri().toString(), e);
|
Log.e(TAG, "Failed to truncate the file: " + storage.getUri().toString(), e);
|
||||||
showFailedDialog(R.string.overwrite_failed);
|
showFailedDialog(R.string.overwrite_failed);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -992,8 +976,8 @@ public class DownloadDialog extends DialogFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
psArgs = null;
|
psArgs = null;
|
||||||
final long videoSize = wrappedVideoStreams
|
final long videoSize = wrappedVideoStreams.getSizeInBytes(
|
||||||
.getSizeInBytes((VideoStream) selectedStream);
|
(VideoStream) selectedStream);
|
||||||
|
|
||||||
// set nearLength, only, if both sizes are fetched or known. This probably
|
// set nearLength, only, if both sizes are fetched or known. This probably
|
||||||
// does not work on slow networks but is later updated in the downloader
|
// does not work on slow networks but is later updated in the downloader
|
||||||
|
@ -1009,7 +993,7 @@ public class DownloadDialog extends DialogFragment
|
||||||
|
|
||||||
if (selectedStream.getFormat() == MediaFormat.TTML) {
|
if (selectedStream.getFormat() == MediaFormat.TTML) {
|
||||||
psName = Postprocessing.ALGORITHM_TTML_CONVERTER;
|
psName = Postprocessing.ALGORITHM_TTML_CONVERTER;
|
||||||
psArgs = new String[]{
|
psArgs = new String[] {
|
||||||
selectedStream.getFormat().getSuffix(),
|
selectedStream.getFormat().getSuffix(),
|
||||||
"false" // ignore empty frames
|
"false" // ignore empty frames
|
||||||
};
|
};
|
||||||
|
@ -1020,17 +1004,22 @@ public class DownloadDialog extends DialogFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
if (secondaryStream == null) {
|
if (secondaryStream == null) {
|
||||||
urls = new String[]{
|
urls = new String[] {
|
||||||
selectedStream.getUrl()
|
selectedStream.getContent()
|
||||||
};
|
};
|
||||||
recoveryInfo = new MissionRecoveryInfo[]{
|
recoveryInfo = new MissionRecoveryInfo[] {
|
||||||
new MissionRecoveryInfo(selectedStream)
|
new MissionRecoveryInfo(selectedStream)
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
urls = new String[]{
|
if (secondaryStream.getDeliveryMethod() != PROGRESSIVE_HTTP) {
|
||||||
selectedStream.getUrl(), secondaryStream.getUrl()
|
throw new IllegalArgumentException("Unsupported stream delivery format"
|
||||||
|
+ secondaryStream.getDeliveryMethod());
|
||||||
|
}
|
||||||
|
|
||||||
|
urls = new String[] {
|
||||||
|
selectedStream.getContent(), secondaryStream.getContent()
|
||||||
};
|
};
|
||||||
recoveryInfo = new MissionRecoveryInfo[]{new MissionRecoveryInfo(selectedStream),
|
recoveryInfo = new MissionRecoveryInfo[] {new MissionRecoveryInfo(selectedStream),
|
||||||
new MissionRecoveryInfo(secondaryStream)};
|
new MissionRecoveryInfo(secondaryStream)};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,7 @@ import android.view.WindowManager;
|
||||||
import android.view.animation.DecelerateInterpolator;
|
import android.view.animation.DecelerateInterpolator;
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
import android.widget.RelativeLayout;
|
import android.widget.RelativeLayout;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.AttrRes;
|
import androidx.annotation.AttrRes;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
@ -94,6 +95,7 @@ import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.PermissionHelper;
|
import org.schabi.newpipe.util.PermissionHelper;
|
||||||
import org.schabi.newpipe.util.PicassoHelper;
|
import org.schabi.newpipe.util.PicassoHelper;
|
||||||
|
import org.schabi.newpipe.util.StreamTypeUtil;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
|
@ -121,6 +123,7 @@ import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientat
|
||||||
import static org.schabi.newpipe.player.helper.PlayerHelper.isClearingQueueConfirmationRequired;
|
import static org.schabi.newpipe.player.helper.PlayerHelper.isClearingQueueConfirmationRequired;
|
||||||
import static org.schabi.newpipe.player.playqueue.PlayQueueItem.RECOVERY_UNSET;
|
import static org.schabi.newpipe.player.playqueue.PlayQueueItem.RECOVERY_UNSET;
|
||||||
import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
|
import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
|
||||||
|
import static org.schabi.newpipe.util.ListHelper.getUrlAndNonTorrentStreams;
|
||||||
|
|
||||||
public final class VideoDetailFragment
|
public final class VideoDetailFragment
|
||||||
extends BaseStateFragment<StreamInfo>
|
extends BaseStateFragment<StreamInfo>
|
||||||
|
@ -186,8 +189,6 @@ public final class VideoDetailFragment
|
||||||
@Nullable
|
@Nullable
|
||||||
private Disposable positionSubscriber = null;
|
private Disposable positionSubscriber = null;
|
||||||
|
|
||||||
private List<VideoStream> sortedVideoStreams;
|
|
||||||
private int selectedVideoStreamIndex = -1;
|
|
||||||
private BottomSheetBehavior<FrameLayout> bottomSheetBehavior;
|
private BottomSheetBehavior<FrameLayout> bottomSheetBehavior;
|
||||||
private BroadcastReceiver broadcastReceiver;
|
private BroadcastReceiver broadcastReceiver;
|
||||||
|
|
||||||
|
@ -1092,9 +1093,6 @@ public final class VideoDetailFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
private void openBackgroundPlayer(final boolean append) {
|
private void openBackgroundPlayer(final boolean append) {
|
||||||
final AudioStream audioStream = currentInfo.getAudioStreams()
|
|
||||||
.get(ListHelper.getDefaultAudioFormat(activity, currentInfo.getAudioStreams()));
|
|
||||||
|
|
||||||
final boolean useExternalAudioPlayer = PreferenceManager
|
final boolean useExternalAudioPlayer = PreferenceManager
|
||||||
.getDefaultSharedPreferences(activity)
|
.getDefaultSharedPreferences(activity)
|
||||||
.getBoolean(activity.getString(R.string.use_external_audio_player_key), false);
|
.getBoolean(activity.getString(R.string.use_external_audio_player_key), false);
|
||||||
|
@ -1109,7 +1107,17 @@ public final class VideoDetailFragment
|
||||||
if (!useExternalAudioPlayer) {
|
if (!useExternalAudioPlayer) {
|
||||||
openNormalBackgroundPlayer(append);
|
openNormalBackgroundPlayer(append);
|
||||||
} else {
|
} else {
|
||||||
startOnExternalPlayer(activity, currentInfo, audioStream);
|
final List<AudioStream> audioStreams = getUrlAndNonTorrentStreams(
|
||||||
|
currentInfo.getAudioStreams());
|
||||||
|
final int index = ListHelper.getDefaultAudioFormat(activity, audioStreams);
|
||||||
|
|
||||||
|
if (index == -1) {
|
||||||
|
Toast.makeText(activity, R.string.no_audio_streams_available_for_external_players,
|
||||||
|
Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
startOnExternalPlayer(activity, currentInfo, audioStreams.get(index));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1612,14 +1620,6 @@ public final class VideoDetailFragment
|
||||||
binding.detailToggleSecondaryControlsView.setVisibility(View.VISIBLE);
|
binding.detailToggleSecondaryControlsView.setVisibility(View.VISIBLE);
|
||||||
binding.detailSecondaryControlPanel.setVisibility(View.GONE);
|
binding.detailSecondaryControlPanel.setVisibility(View.GONE);
|
||||||
|
|
||||||
sortedVideoStreams = ListHelper.getSortedStreamVideosList(
|
|
||||||
activity,
|
|
||||||
info.getVideoStreams(),
|
|
||||||
info.getVideoOnlyStreams(),
|
|
||||||
false,
|
|
||||||
false);
|
|
||||||
selectedVideoStreamIndex = ListHelper
|
|
||||||
.getDefaultResolutionIndex(activity, sortedVideoStreams);
|
|
||||||
updateProgressInfo(info);
|
updateProgressInfo(info);
|
||||||
initThumbnailViews(info);
|
initThumbnailViews(info);
|
||||||
showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView,
|
showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView,
|
||||||
|
@ -1645,8 +1645,8 @@ public final class VideoDetailFragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.detailControlsDownload.setVisibility(info.getStreamType() == StreamType.LIVE_STREAM
|
binding.detailControlsDownload.setVisibility(
|
||||||
|| info.getStreamType() == StreamType.AUDIO_LIVE_STREAM ? View.GONE : View.VISIBLE);
|
StreamTypeUtil.isLiveStream(info.getStreamType()) ? View.GONE : View.VISIBLE);
|
||||||
binding.detailControlsBackground.setVisibility(info.getAudioStreams().isEmpty()
|
binding.detailControlsBackground.setVisibility(info.getAudioStreams().isEmpty()
|
||||||
? View.GONE : View.VISIBLE);
|
? View.GONE : View.VISIBLE);
|
||||||
|
|
||||||
|
@ -1687,12 +1687,7 @@ public final class VideoDetailFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final DownloadDialog downloadDialog = DownloadDialog.newInstance(currentInfo);
|
final DownloadDialog downloadDialog = new DownloadDialog(activity, currentInfo);
|
||||||
downloadDialog.setVideoStreams(sortedVideoStreams);
|
|
||||||
downloadDialog.setAudioStreams(currentInfo.getAudioStreams());
|
|
||||||
downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex);
|
|
||||||
downloadDialog.setSubtitleStreams(currentInfo.getSubtitles());
|
|
||||||
|
|
||||||
downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
|
downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
ErrorUtil.showSnackbar(activity, new ErrorInfo(e, UserAction.DOWNLOAD_OPEN_DIALOG,
|
ErrorUtil.showSnackbar(activity, new ErrorInfo(e, UserAction.DOWNLOAD_OPEN_DIALOG,
|
||||||
|
@ -1722,8 +1717,7 @@ public final class VideoDetailFragment
|
||||||
binding.detailPositionView.setVisibility(View.GONE);
|
binding.detailPositionView.setVisibility(View.GONE);
|
||||||
// TODO: Remove this check when separation of concerns is done.
|
// TODO: Remove this check when separation of concerns is done.
|
||||||
// (live streams weren't getting updated because they are mixed)
|
// (live streams weren't getting updated because they are mixed)
|
||||||
if (!info.getStreamType().equals(StreamType.LIVE_STREAM)
|
if (!StreamTypeUtil.isLiveStream(info.getStreamType())) {
|
||||||
&& !info.getStreamType().equals(StreamType.AUDIO_LIVE_STREAM)) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -2151,25 +2145,52 @@ public final class VideoDetailFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showExternalPlaybackDialog() {
|
private void showExternalPlaybackDialog() {
|
||||||
if (sortedVideoStreams == null) {
|
if (currentInfo == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final CharSequence[] resolutions = new CharSequence[sortedVideoStreams.size()];
|
|
||||||
for (int i = 0; i < sortedVideoStreams.size(); i++) {
|
final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||||
resolutions[i] = sortedVideoStreams.get(i).getResolution();
|
builder.setTitle(R.string.select_quality_external_players);
|
||||||
}
|
builder.setNeutralButton(R.string.open_in_browser, (dialog, i) ->
|
||||||
final AlertDialog.Builder builder = new AlertDialog.Builder(activity)
|
ShareUtils.openUrlInBrowser(requireActivity(), url));
|
||||||
.setNegativeButton(R.string.cancel, null)
|
|
||||||
.setNeutralButton(R.string.open_in_browser, (dialog, i) ->
|
final List<VideoStream> videoStreamsForExternalPlayers =
|
||||||
ShareUtils.openUrlInBrowser(requireActivity(), url)
|
ListHelper.getSortedStreamVideosList(
|
||||||
|
activity,
|
||||||
|
getUrlAndNonTorrentStreams(currentInfo.getVideoStreams()),
|
||||||
|
getUrlAndNonTorrentStreams(currentInfo.getVideoOnlyStreams()),
|
||||||
|
false,
|
||||||
|
false
|
||||||
);
|
);
|
||||||
// Maybe there are no video streams available, show just `open in browser` button
|
|
||||||
if (resolutions.length > 0) {
|
if (videoStreamsForExternalPlayers.isEmpty()) {
|
||||||
builder.setSingleChoiceItems(resolutions, selectedVideoStreamIndex, (dialog, i) -> {
|
builder.setMessage(R.string.no_video_streams_available_for_external_players);
|
||||||
dialog.dismiss();
|
builder.setPositiveButton(R.string.ok, null);
|
||||||
startOnExternalPlayer(activity, currentInfo, sortedVideoStreams.get(i));
|
|
||||||
}
|
} else {
|
||||||
);
|
final int selectedVideoStreamIndexForExternalPlayers =
|
||||||
|
ListHelper.getDefaultResolutionIndex(activity, videoStreamsForExternalPlayers);
|
||||||
|
final CharSequence[] resolutions =
|
||||||
|
new CharSequence[videoStreamsForExternalPlayers.size()];
|
||||||
|
|
||||||
|
for (int i = 0; i < videoStreamsForExternalPlayers.size(); i++) {
|
||||||
|
resolutions[i] = videoStreamsForExternalPlayers.get(i).getResolution();
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.setSingleChoiceItems(resolutions, selectedVideoStreamIndexForExternalPlayers,
|
||||||
|
null);
|
||||||
|
builder.setNegativeButton(R.string.cancel, null);
|
||||||
|
builder.setPositiveButton(R.string.ok, (dialog, i) -> {
|
||||||
|
final int index = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
|
||||||
|
// We don't have to manage the index validity because if there is no stream
|
||||||
|
// available for external players, this code will be not executed and if there is
|
||||||
|
// no stream which matches the default resolution, 0 is returned by
|
||||||
|
// ListHelper.getDefaultResolutionIndex.
|
||||||
|
// The index cannot be outside the bounds of the list as its always between 0 and
|
||||||
|
// the list size - 1, .
|
||||||
|
startOnExternalPlayer(activity, currentInfo,
|
||||||
|
videoStreamsForExternalPlayers.get(index));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
builder.show();
|
builder.show();
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||||
|
import org.schabi.newpipe.util.StreamTypeUtil;
|
||||||
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -269,8 +270,7 @@ public final class InfoItemDialog {
|
||||||
*/
|
*/
|
||||||
public Builder addStartHereEntries() {
|
public Builder addStartHereEntries() {
|
||||||
addEntry(StreamDialogDefaultEntry.START_HERE_ON_BACKGROUND);
|
addEntry(StreamDialogDefaultEntry.START_HERE_ON_BACKGROUND);
|
||||||
if (infoItem.getStreamType() != StreamType.AUDIO_STREAM
|
if (!StreamTypeUtil.isAudio(infoItem.getStreamType())) {
|
||||||
&& infoItem.getStreamType() != StreamType.AUDIO_LIVE_STREAM) {
|
|
||||||
addEntry(StreamDialogDefaultEntry.START_HERE_ON_POPUP);
|
addEntry(StreamDialogDefaultEntry.START_HERE_ON_POPUP);
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
|
@ -285,9 +285,7 @@ public final class InfoItemDialog {
|
||||||
final boolean isWatchHistoryEnabled = PreferenceManager
|
final boolean isWatchHistoryEnabled = PreferenceManager
|
||||||
.getDefaultSharedPreferences(context)
|
.getDefaultSharedPreferences(context)
|
||||||
.getBoolean(context.getString(R.string.enable_watch_history_key), false);
|
.getBoolean(context.getString(R.string.enable_watch_history_key), false);
|
||||||
if (isWatchHistoryEnabled
|
if (isWatchHistoryEnabled && !StreamTypeUtil.isLiveStream(infoItem.getStreamType())) {
|
||||||
&& infoItem.getStreamType() != StreamType.LIVE_STREAM
|
|
||||||
&& infoItem.getStreamType() != StreamType.AUDIO_LIVE_STREAM) {
|
|
||||||
addEntry(StreamDialogDefaultEntry.MARK_AS_WATCHED);
|
addEntry(StreamDialogDefaultEntry.MARK_AS_WATCHED);
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
|
|
|
@ -11,12 +11,12 @@ import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
|
||||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||||
import org.schabi.newpipe.ktx.ViewUtils;
|
import org.schabi.newpipe.ktx.ViewUtils;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.PicassoHelper;
|
import org.schabi.newpipe.util.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
import org.schabi.newpipe.util.StreamTypeUtil;
|
||||||
import org.schabi.newpipe.views.AnimatedProgressBar;
|
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||||
|
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
@ -70,8 +70,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
||||||
} else {
|
} else {
|
||||||
itemProgressView.setVisibility(View.GONE);
|
itemProgressView.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
} else if (item.getStreamType() == StreamType.LIVE_STREAM
|
} else if (StreamTypeUtil.isLiveStream(item.getStreamType())) {
|
||||||
|| item.getStreamType() == StreamType.AUDIO_LIVE_STREAM) {
|
|
||||||
itemDurationView.setText(R.string.duration_live);
|
itemDurationView.setText(R.string.duration_live);
|
||||||
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(),
|
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(),
|
||||||
R.color.live_duration_background_color));
|
R.color.live_duration_background_color));
|
||||||
|
@ -96,9 +95,10 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
||||||
case VIDEO_STREAM:
|
case VIDEO_STREAM:
|
||||||
case LIVE_STREAM:
|
case LIVE_STREAM:
|
||||||
case AUDIO_LIVE_STREAM:
|
case AUDIO_LIVE_STREAM:
|
||||||
|
case POST_LIVE_STREAM:
|
||||||
|
case POST_LIVE_AUDIO_STREAM:
|
||||||
enableLongClick(item);
|
enableLongClick(item);
|
||||||
break;
|
break;
|
||||||
case FILE:
|
|
||||||
case NONE:
|
case NONE:
|
||||||
default:
|
default:
|
||||||
disableLongClick();
|
disableLongClick();
|
||||||
|
@ -114,7 +114,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
||||||
final StreamStateEntity state
|
final StreamStateEntity state
|
||||||
= historyRecordManager.loadStreamState(infoItem).blockingGet()[0];
|
= historyRecordManager.loadStreamState(infoItem).blockingGet()[0];
|
||||||
if (state != null && item.getDuration() > 0
|
if (state != null && item.getDuration() > 0
|
||||||
&& item.getStreamType() != StreamType.LIVE_STREAM) {
|
&& !StreamTypeUtil.isLiveStream(item.getStreamType())) {
|
||||||
itemProgressView.setMax((int) item.getDuration());
|
itemProgressView.setMax((int) item.getDuration());
|
||||||
if (itemProgressView.getVisibility() == View.VISIBLE) {
|
if (itemProgressView.getVisibility() == View.VISIBLE) {
|
||||||
itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS
|
itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS
|
||||||
|
|
|
@ -14,6 +14,8 @@ import org.schabi.newpipe.databinding.ListStreamItemBinding
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_LIVE_STREAM
|
import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_LIVE_STREAM
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_STREAM
|
import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_STREAM
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType.LIVE_STREAM
|
import org.schabi.newpipe.extractor.stream.StreamType.LIVE_STREAM
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamType.POST_LIVE_AUDIO_STREAM
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamType.POST_LIVE_STREAM
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType.VIDEO_STREAM
|
import org.schabi.newpipe.extractor.stream.StreamType.VIDEO_STREAM
|
||||||
import org.schabi.newpipe.util.Localization
|
import org.schabi.newpipe.util.Localization
|
||||||
import org.schabi.newpipe.util.PicassoHelper
|
import org.schabi.newpipe.util.PicassoHelper
|
||||||
|
@ -109,7 +111,7 @@ data class StreamItem(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isLongClickable() = when (stream.streamType) {
|
override fun isLongClickable() = when (stream.streamType) {
|
||||||
AUDIO_STREAM, VIDEO_STREAM, LIVE_STREAM, AUDIO_LIVE_STREAM -> true
|
AUDIO_STREAM, VIDEO_STREAM, LIVE_STREAM, AUDIO_LIVE_STREAM, POST_LIVE_STREAM, POST_LIVE_AUDIO_STREAM -> true
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -150,7 +150,6 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||||
import com.squareup.picasso.Picasso;
|
import com.squareup.picasso.Picasso;
|
||||||
import com.squareup.picasso.Target;
|
import com.squareup.picasso.Target;
|
||||||
|
|
||||||
import org.schabi.newpipe.DownloaderImpl;
|
|
||||||
import org.schabi.newpipe.MainActivity;
|
import org.schabi.newpipe.MainActivity;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||||
|
@ -429,7 +428,7 @@ public final class Player implements
|
||||||
setupBroadcastReceiver();
|
setupBroadcastReceiver();
|
||||||
|
|
||||||
trackSelector = new DefaultTrackSelector(context, PlayerHelper.getQualitySelector());
|
trackSelector = new DefaultTrackSelector(context, PlayerHelper.getQualitySelector());
|
||||||
final PlayerDataSource dataSource = new PlayerDataSource(context, DownloaderImpl.USER_AGENT,
|
final PlayerDataSource dataSource = new PlayerDataSource(context,
|
||||||
new DefaultBandwidthMeter.Builder(context).build());
|
new DefaultBandwidthMeter.Builder(context).build());
|
||||||
loadController = new LoadController();
|
loadController = new LoadController();
|
||||||
renderFactory = new DefaultRenderersFactory(context);
|
renderFactory = new DefaultRenderersFactory(context);
|
||||||
|
@ -1744,24 +1743,9 @@ public final class Player implements
|
||||||
if (exoPlayerIsNull()) {
|
if (exoPlayerIsNull()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Use duration of currentItem for non-live streams,
|
|
||||||
// because HLS streams are fragmented
|
onUpdateProgress(Math.max((int) simpleExoPlayer.getCurrentPosition(), 0),
|
||||||
// and thus the whole duration is not available to the player
|
(int) simpleExoPlayer.getDuration(), simpleExoPlayer.getBufferedPercentage());
|
||||||
// TODO: revert #6307 when introducing proper HLS support
|
|
||||||
final int duration;
|
|
||||||
if (currentItem != null
|
|
||||||
&& !StreamTypeUtil.isLiveStream(currentItem.getStreamType())
|
|
||||||
) {
|
|
||||||
// convert seconds to milliseconds
|
|
||||||
duration = (int) (currentItem.getDuration() * 1000);
|
|
||||||
} else {
|
|
||||||
duration = (int) simpleExoPlayer.getDuration();
|
|
||||||
}
|
|
||||||
onUpdateProgress(
|
|
||||||
Math.max((int) simpleExoPlayer.getCurrentPosition(), 0),
|
|
||||||
duration,
|
|
||||||
simpleExoPlayer.getBufferedPercentage()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Disposable getProgressUpdateDisposable() {
|
private Disposable getProgressUpdateDisposable() {
|
||||||
|
@ -3399,6 +3383,7 @@ public final class Player implements
|
||||||
|
|
||||||
switch (info.getStreamType()) {
|
switch (info.getStreamType()) {
|
||||||
case AUDIO_STREAM:
|
case AUDIO_STREAM:
|
||||||
|
case POST_LIVE_AUDIO_STREAM:
|
||||||
binding.surfaceView.setVisibility(View.GONE);
|
binding.surfaceView.setVisibility(View.GONE);
|
||||||
binding.endScreen.setVisibility(View.VISIBLE);
|
binding.endScreen.setVisibility(View.VISIBLE);
|
||||||
binding.playbackEndTime.setVisibility(View.VISIBLE);
|
binding.playbackEndTime.setVisibility(View.VISIBLE);
|
||||||
|
@ -3417,6 +3402,7 @@ public final class Player implements
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case VIDEO_STREAM:
|
case VIDEO_STREAM:
|
||||||
|
case POST_LIVE_STREAM:
|
||||||
if (currentMetadata == null
|
if (currentMetadata == null
|
||||||
|| !currentMetadata.getMaybeQuality().isPresent()
|
|| !currentMetadata.getMaybeQuality().isPresent()
|
||||||
|| (info.getVideoStreams().isEmpty()
|
|| (info.getVideoStreams().isEmpty()
|
||||||
|
@ -3484,10 +3470,10 @@ public final class Player implements
|
||||||
for (int i = 0; i < availableStreams.size(); i++) {
|
for (int i = 0; i < availableStreams.size(); i++) {
|
||||||
final VideoStream videoStream = availableStreams.get(i);
|
final VideoStream videoStream = availableStreams.get(i);
|
||||||
qualityPopupMenu.getMenu().add(POPUP_MENU_ID_QUALITY, i, Menu.NONE, MediaFormat
|
qualityPopupMenu.getMenu().add(POPUP_MENU_ID_QUALITY, i, Menu.NONE, MediaFormat
|
||||||
.getNameById(videoStream.getFormatId()) + " " + videoStream.resolution);
|
.getNameById(videoStream.getFormatId()) + " " + videoStream.getResolution());
|
||||||
}
|
}
|
||||||
if (getSelectedVideoStream() != null) {
|
if (getSelectedVideoStream() != null) {
|
||||||
binding.qualityTextView.setText(getSelectedVideoStream().resolution);
|
binding.qualityTextView.setText(getSelectedVideoStream().getResolution());
|
||||||
}
|
}
|
||||||
qualityPopupMenu.setOnMenuItemClickListener(this);
|
qualityPopupMenu.setOnMenuItemClickListener(this);
|
||||||
qualityPopupMenu.setOnDismissListener(this);
|
qualityPopupMenu.setOnDismissListener(this);
|
||||||
|
@ -3605,7 +3591,7 @@ public final class Player implements
|
||||||
}
|
}
|
||||||
|
|
||||||
saveStreamProgressState(); //TODO added, check if good
|
saveStreamProgressState(); //TODO added, check if good
|
||||||
final String newResolution = availableStreams.get(menuItemIndex).resolution;
|
final String newResolution = availableStreams.get(menuItemIndex).getResolution();
|
||||||
setRecovery();
|
setRecovery();
|
||||||
setPlaybackQuality(newResolution);
|
setPlaybackQuality(newResolution);
|
||||||
reloadPlayQueueManager();
|
reloadPlayQueueManager();
|
||||||
|
@ -3633,7 +3619,7 @@ public final class Player implements
|
||||||
}
|
}
|
||||||
isSomePopupMenuVisible = false; //TODO check if this works
|
isSomePopupMenuVisible = false; //TODO check if this works
|
||||||
if (getSelectedVideoStream() != null) {
|
if (getSelectedVideoStream() != null) {
|
||||||
binding.qualityTextView.setText(getSelectedVideoStream().resolution);
|
binding.qualityTextView.setText(getSelectedVideoStream().getResolution());
|
||||||
}
|
}
|
||||||
if (isPlaying()) {
|
if (isPlaying()) {
|
||||||
hideControls(DEFAULT_CONTROLS_DURATION, 0);
|
hideControls(DEFAULT_CONTROLS_DURATION, 0);
|
||||||
|
@ -4248,9 +4234,7 @@ public final class Player implements
|
||||||
if (playQueueManagerReloadingNeeded(sourceType, info, getVideoRendererIndex())) {
|
if (playQueueManagerReloadingNeeded(sourceType, info, getVideoRendererIndex())) {
|
||||||
reloadPlayQueueManager();
|
reloadPlayQueueManager();
|
||||||
} else {
|
} else {
|
||||||
final StreamType streamType = info.getStreamType();
|
if (StreamTypeUtil.isAudio(info.getStreamType())) {
|
||||||
if (streamType == StreamType.AUDIO_STREAM
|
|
||||||
|| streamType == StreamType.AUDIO_LIVE_STREAM) {
|
|
||||||
// Nothing to do more than setting the recovery position
|
// Nothing to do more than setting the recovery position
|
||||||
setRecovery();
|
setRecovery();
|
||||||
return;
|
return;
|
||||||
|
@ -4285,13 +4269,15 @@ public final class Player implements
|
||||||
* the content is not an audio content, but also if none of the following cases is met:
|
* the content is not an audio content, but also if none of the following cases is met:
|
||||||
*
|
*
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>the content is an {@link StreamType#AUDIO_STREAM audio stream} or an
|
* <li>the content is an {@link StreamType#AUDIO_STREAM audio stream}, an
|
||||||
* {@link StreamType#AUDIO_LIVE_STREAM audio live stream};</li>
|
* {@link StreamType#AUDIO_LIVE_STREAM audio live stream}, or a
|
||||||
|
* {@link StreamType#POST_LIVE_AUDIO_STREAM ended audio live stream};</li>
|
||||||
* <li>the content is a {@link StreamType#LIVE_STREAM live stream} and the source type is a
|
* <li>the content is a {@link StreamType#LIVE_STREAM live stream} and the source type is a
|
||||||
* {@link SourceType#LIVE_STREAM live source};</li>
|
* {@link SourceType#LIVE_STREAM live source};</li>
|
||||||
* <li>the content's source is {@link SourceType#VIDEO_WITH_SEPARATED_AUDIO a video stream
|
* <li>the content's source is {@link SourceType#VIDEO_WITH_SEPARATED_AUDIO a video stream
|
||||||
* with a separated audio source} or has no audio-only streams available <b>and</b> is a
|
* with a separated audio source} or has no audio-only streams available <b>and</b> is a
|
||||||
* {@link StreamType#LIVE_STREAM live stream} or a
|
* {@link StreamType#VIDEO_STREAM video stream}, an
|
||||||
|
* {@link StreamType#POST_LIVE_STREAM ended live stream}, or a
|
||||||
* {@link StreamType#LIVE_STREAM live stream}.
|
* {@link StreamType#LIVE_STREAM live stream}.
|
||||||
* </li>
|
* </li>
|
||||||
* </ul>
|
* </ul>
|
||||||
|
@ -4307,18 +4293,17 @@ public final class Player implements
|
||||||
@NonNull final StreamInfo streamInfo,
|
@NonNull final StreamInfo streamInfo,
|
||||||
final int videoRendererIndex) {
|
final int videoRendererIndex) {
|
||||||
final StreamType streamType = streamInfo.getStreamType();
|
final StreamType streamType = streamInfo.getStreamType();
|
||||||
|
final boolean isStreamTypeAudio = StreamTypeUtil.isAudio(streamType);
|
||||||
|
|
||||||
if (videoRendererIndex == RENDERER_UNAVAILABLE && streamType != StreamType.AUDIO_STREAM
|
if (videoRendererIndex == RENDERER_UNAVAILABLE && !isStreamTypeAudio) {
|
||||||
&& streamType != StreamType.AUDIO_LIVE_STREAM) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The content is an audio stream, an audio live stream, or a live stream with a live
|
// The content is an audio stream, an audio live stream, or a live stream with a live
|
||||||
// source: it's not needed to reload the play queue manager because the stream source will
|
// source: it's not needed to reload the play queue manager because the stream source will
|
||||||
// be the same
|
// be the same
|
||||||
if ((streamType == StreamType.AUDIO_STREAM || streamType == StreamType.AUDIO_LIVE_STREAM)
|
if (isStreamTypeAudio || (streamType == StreamType.LIVE_STREAM
|
||||||
|| (streamType == StreamType.LIVE_STREAM
|
&& sourceType == SourceType.LIVE_STREAM)) {
|
||||||
&& sourceType == SourceType.LIVE_STREAM)) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4331,8 +4316,8 @@ public final class Player implements
|
||||||
|| (sourceType == SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY
|
|| (sourceType == SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY
|
||||||
&& isNullOrEmpty(streamInfo.getAudioStreams()))) {
|
&& isNullOrEmpty(streamInfo.getAudioStreams()))) {
|
||||||
// It's not needed to reload the play queue manager only if the content's stream type
|
// It's not needed to reload the play queue manager only if the content's stream type
|
||||||
// is a video stream or a live stream
|
// is a video stream, a live stream or an ended live stream
|
||||||
return streamType != StreamType.VIDEO_STREAM && streamType != StreamType.LIVE_STREAM;
|
return !StreamTypeUtil.isVideo(streamType);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Other cases: the play queue manager reload is needed
|
// Other cases: the play queue manager reload is needed
|
||||||
|
|
|
@ -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;
|
package org.schabi.newpipe.player.helper;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import com.google.android.exoplayer2.database.StandaloneDatabaseProvider;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.upstream.DataSource;
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultDataSource;
|
import com.google.android.exoplayer2.upstream.DefaultDataSource;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
|
|
||||||
import com.google.android.exoplayer2.upstream.FileDataSource;
|
import com.google.android.exoplayer2.upstream.FileDataSource;
|
||||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||||
import com.google.android.exoplayer2.upstream.cache.CacheDataSink;
|
import com.google.android.exoplayer2.upstream.cache.CacheDataSink;
|
||||||
import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
|
import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
|
||||||
import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor;
|
|
||||||
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
|
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
|
||||||
|
|
||||||
import java.io.File;
|
final class CacheFactory implements DataSource.Factory {
|
||||||
|
private static final int CACHE_FLAGS = CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
private final Context context;
|
||||||
|
private final TransferListener transferListener;
|
||||||
|
private final DataSource.Factory upstreamDataSourceFactory;
|
||||||
|
private final SimpleCache cache;
|
||||||
|
|
||||||
/* package-private */ class CacheFactory implements DataSource.Factory {
|
CacheFactory(final Context context,
|
||||||
private static final String TAG = "CacheFactory";
|
final TransferListener transferListener,
|
||||||
|
final SimpleCache cache,
|
||||||
private static final String CACHE_FOLDER_NAME = "exoplayer";
|
final DataSource.Factory upstreamDataSourceFactory) {
|
||||||
private static final int CACHE_FLAGS = CacheDataSource.FLAG_BLOCK_ON_CACHE
|
this.context = context;
|
||||||
| CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR;
|
this.transferListener = transferListener;
|
||||||
|
this.cache = cache;
|
||||||
private final DataSource.Factory dataSourceFactory;
|
this.upstreamDataSourceFactory = upstreamDataSourceFactory;
|
||||||
private final File cacheDir;
|
|
||||||
private final long maxFileSize;
|
|
||||||
|
|
||||||
// Creating cache on every instance may cause problems with multiple players when
|
|
||||||
// sources are not ExtractorMediaSource
|
|
||||||
// see: https://stackoverflow.com/questions/28700391/using-cache-in-exoplayer
|
|
||||||
// todo: make this a singleton?
|
|
||||||
private static SimpleCache cache;
|
|
||||||
|
|
||||||
CacheFactory(@NonNull final Context context,
|
|
||||||
@NonNull final String userAgent,
|
|
||||||
@NonNull final TransferListener transferListener) {
|
|
||||||
this(context, userAgent, transferListener, PlayerHelper.getPreferredCacheSize(),
|
|
||||||
PlayerHelper.getPreferredFileSize());
|
|
||||||
}
|
|
||||||
|
|
||||||
private CacheFactory(@NonNull final Context context,
|
|
||||||
@NonNull final String userAgent,
|
|
||||||
@NonNull final TransferListener transferListener,
|
|
||||||
final long maxCacheSize,
|
|
||||||
final long maxFileSize) {
|
|
||||||
this.maxFileSize = maxFileSize;
|
|
||||||
|
|
||||||
dataSourceFactory = new DefaultDataSource
|
|
||||||
.Factory(context, new DefaultHttpDataSource.Factory().setUserAgent(userAgent))
|
|
||||||
.setTransferListener(transferListener);
|
|
||||||
cacheDir = new File(context.getExternalCacheDir(), CACHE_FOLDER_NAME);
|
|
||||||
if (!cacheDir.exists()) {
|
|
||||||
//noinspection ResultOfMethodCallIgnored
|
|
||||||
cacheDir.mkdir();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cache == null) {
|
|
||||||
final LeastRecentlyUsedCacheEvictor evictor
|
|
||||||
= new LeastRecentlyUsedCacheEvictor(maxCacheSize);
|
|
||||||
cache = new SimpleCache(cacheDir, evictor, new StandaloneDatabaseProvider(context));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public DataSource createDataSource() {
|
public DataSource createDataSource() {
|
||||||
Log.d(TAG, "initExoPlayerCache: cacheDir = " + cacheDir.getAbsolutePath());
|
final DefaultDataSource dataSource = new DefaultDataSource.Factory(context,
|
||||||
|
upstreamDataSourceFactory)
|
||||||
|
.setTransferListener(transferListener)
|
||||||
|
.createDataSource();
|
||||||
|
|
||||||
final DataSource dataSource = dataSourceFactory.createDataSource();
|
|
||||||
final FileDataSource fileSource = new FileDataSource();
|
final FileDataSource fileSource = new FileDataSource();
|
||||||
final CacheDataSink dataSink = new CacheDataSink(cache, maxFileSize);
|
final CacheDataSink dataSink
|
||||||
|
= new CacheDataSink(cache, PlayerHelper.getPreferredFileSize());
|
||||||
return new CacheDataSource(cache, dataSource, fileSource, dataSink, CACHE_FLAGS, null);
|
return new CacheDataSource(cache, dataSource, fileSource, dataSink, CACHE_FLAGS, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void tryDeleteCacheFiles() {
|
|
||||||
if (!cacheDir.exists() || !cacheDir.isDirectory()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
for (final File file : cacheDir.listFiles()) {
|
|
||||||
final String filePath = file.getAbsolutePath();
|
|
||||||
final boolean deleteSuccessful = file.delete();
|
|
||||||
|
|
||||||
Log.d(TAG, "tryDeleteCacheFiles: " + filePath + " deleted = " + deleteSuccessful);
|
|
||||||
}
|
|
||||||
} catch (final Exception e) {
|
|
||||||
Log.e(TAG, "Failed to delete file.", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
package org.schabi.newpipe.player.helper;
|
package org.schabi.newpipe.player.helper;
|
||||||
|
|
||||||
import android.content.Context;
|
import static org.schabi.newpipe.MainActivity.DEBUG;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.database.StandaloneDatabaseProvider;
|
||||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||||
import com.google.android.exoplayer2.source.SingleSampleMediaSource;
|
import com.google.android.exoplayer2.source.SingleSampleMediaSource;
|
||||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||||
|
@ -13,12 +19,21 @@ import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
||||||
import com.google.android.exoplayer2.upstream.DataSource;
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultDataSource;
|
import com.google.android.exoplayer2.upstream.DefaultDataSource;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
|
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
|
|
||||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||||
|
import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor;
|
||||||
|
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import org.schabi.newpipe.DownloaderImpl;
|
||||||
|
import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeOtfDashManifestCreator;
|
||||||
|
import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubePostLiveStreamDvrDashManifestCreator;
|
||||||
|
import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeProgressiveDashManifestCreator;
|
||||||
|
import org.schabi.newpipe.player.datasource.NonUriHlsDataSourceFactory;
|
||||||
|
import org.schabi.newpipe.player.datasource.YoutubeHttpDataSource;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
public class PlayerDataSource {
|
public class PlayerDataSource {
|
||||||
|
public static final String TAG = PlayerDataSource.class.getSimpleName();
|
||||||
|
|
||||||
public static final int LIVE_STREAM_EDGE_GAP_MILLIS = 10000;
|
public static final int LIVE_STREAM_EDGE_GAP_MILLIS = 10000;
|
||||||
|
|
||||||
|
@ -29,79 +44,174 @@ public class PlayerDataSource {
|
||||||
* early.
|
* early.
|
||||||
*/
|
*/
|
||||||
private static final double PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT = 15;
|
private static final double PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT = 15;
|
||||||
private static final int MANIFEST_MINIMUM_RETRY = 5;
|
|
||||||
private static final int EXTRACTOR_MINIMUM_RETRY = Integer.MAX_VALUE;
|
|
||||||
|
|
||||||
private final int continueLoadingCheckIntervalBytes;
|
/**
|
||||||
private final DataSource.Factory cacheDataSourceFactory;
|
* The maximum number of generated manifests per cache, in
|
||||||
|
* {@link YoutubeProgressiveDashManifestCreator}, {@link YoutubeOtfDashManifestCreator} and
|
||||||
|
* {@link YoutubePostLiveStreamDvrDashManifestCreator}.
|
||||||
|
*/
|
||||||
|
private static final int MAX_MANIFEST_CACHE_SIZE = 500;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The folder name in which the ExoPlayer cache will be written.
|
||||||
|
*/
|
||||||
|
private static final String CACHE_FOLDER_NAME = "exoplayer";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link SimpleCache} instance which will be used to build
|
||||||
|
* {@link com.google.android.exoplayer2.upstream.cache.CacheDataSource}s instances (with
|
||||||
|
* {@link CacheFactory}).
|
||||||
|
*/
|
||||||
|
private static SimpleCache cache;
|
||||||
|
|
||||||
|
|
||||||
|
private final int progressiveLoadIntervalBytes;
|
||||||
|
|
||||||
|
// Generic Data Source Factories (without or with cache)
|
||||||
private final DataSource.Factory cachelessDataSourceFactory;
|
private final DataSource.Factory cachelessDataSourceFactory;
|
||||||
|
private final CacheFactory cacheDataSourceFactory;
|
||||||
|
|
||||||
public PlayerDataSource(@NonNull final Context context,
|
// YouTube-specific Data Source Factories (with cache)
|
||||||
@NonNull final String userAgent,
|
// They use YoutubeHttpDataSource.Factory, with different parameters each
|
||||||
@NonNull final TransferListener transferListener) {
|
private final CacheFactory ytHlsCacheDataSourceFactory;
|
||||||
continueLoadingCheckIntervalBytes = PlayerHelper.getProgressiveLoadIntervalBytes(context);
|
private final CacheFactory ytDashCacheDataSourceFactory;
|
||||||
cacheDataSourceFactory = new CacheFactory(context, userAgent, transferListener);
|
private final CacheFactory ytProgressiveDashCacheDataSourceFactory;
|
||||||
cachelessDataSourceFactory = new DefaultDataSource
|
|
||||||
.Factory(context, new DefaultHttpDataSource.Factory().setUserAgent(userAgent))
|
|
||||||
|
public PlayerDataSource(final Context context,
|
||||||
|
final TransferListener transferListener) {
|
||||||
|
|
||||||
|
progressiveLoadIntervalBytes = PlayerHelper.getProgressiveLoadIntervalBytes(context);
|
||||||
|
|
||||||
|
// make sure the static cache was created: needed by CacheFactories below
|
||||||
|
instantiateCacheIfNeeded(context);
|
||||||
|
|
||||||
|
// generic data source factories use DefaultHttpDataSource.Factory
|
||||||
|
cachelessDataSourceFactory = new DefaultDataSource.Factory(context,
|
||||||
|
new DefaultHttpDataSource.Factory().setUserAgent(DownloaderImpl.USER_AGENT))
|
||||||
.setTransferListener(transferListener);
|
.setTransferListener(transferListener);
|
||||||
|
cacheDataSourceFactory = new CacheFactory(context, transferListener, cache,
|
||||||
|
new DefaultHttpDataSource.Factory().setUserAgent(DownloaderImpl.USER_AGENT));
|
||||||
|
|
||||||
|
// YouTube-specific data source factories use getYoutubeHttpDataSourceFactory()
|
||||||
|
ytHlsCacheDataSourceFactory = new CacheFactory(context, transferListener, cache,
|
||||||
|
getYoutubeHttpDataSourceFactory(false, false));
|
||||||
|
ytDashCacheDataSourceFactory = new CacheFactory(context, transferListener, cache,
|
||||||
|
getYoutubeHttpDataSourceFactory(true, true));
|
||||||
|
ytProgressiveDashCacheDataSourceFactory = new CacheFactory(context, transferListener, cache,
|
||||||
|
getYoutubeHttpDataSourceFactory(false, true));
|
||||||
|
|
||||||
|
// set the maximum size to manifest creators
|
||||||
|
YoutubeProgressiveDashManifestCreator.getCache().setMaximumSize(MAX_MANIFEST_CACHE_SIZE);
|
||||||
|
YoutubeOtfDashManifestCreator.getCache().setMaximumSize(MAX_MANIFEST_CACHE_SIZE);
|
||||||
|
YoutubePostLiveStreamDvrDashManifestCreator.getCache().setMaximumSize(
|
||||||
|
MAX_MANIFEST_CACHE_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//region Live media source factories
|
||||||
public SsMediaSource.Factory getLiveSsMediaSourceFactory() {
|
public SsMediaSource.Factory getLiveSsMediaSourceFactory() {
|
||||||
return new SsMediaSource.Factory(
|
return getSSMediaSourceFactory().setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS);
|
||||||
new DefaultSsChunkSource.Factory(cachelessDataSourceFactory),
|
|
||||||
cachelessDataSourceFactory
|
|
||||||
)
|
|
||||||
.setLoadErrorHandlingPolicy(
|
|
||||||
new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY))
|
|
||||||
.setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public HlsMediaSource.Factory getLiveHlsMediaSourceFactory() {
|
public HlsMediaSource.Factory getLiveHlsMediaSourceFactory() {
|
||||||
return new HlsMediaSource.Factory(cachelessDataSourceFactory)
|
return new HlsMediaSource.Factory(cachelessDataSourceFactory)
|
||||||
.setAllowChunklessPreparation(true)
|
.setAllowChunklessPreparation(true)
|
||||||
.setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(
|
|
||||||
MANIFEST_MINIMUM_RETRY))
|
|
||||||
.setPlaylistTrackerFactory((dataSourceFactory, loadErrorHandlingPolicy,
|
.setPlaylistTrackerFactory((dataSourceFactory, loadErrorHandlingPolicy,
|
||||||
playlistParserFactory) ->
|
playlistParserFactory) ->
|
||||||
new DefaultHlsPlaylistTracker(dataSourceFactory, loadErrorHandlingPolicy,
|
new DefaultHlsPlaylistTracker(dataSourceFactory, loadErrorHandlingPolicy,
|
||||||
playlistParserFactory, PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT)
|
playlistParserFactory,
|
||||||
);
|
PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT));
|
||||||
}
|
}
|
||||||
|
|
||||||
public DashMediaSource.Factory getLiveDashMediaSourceFactory() {
|
public DashMediaSource.Factory getLiveDashMediaSourceFactory() {
|
||||||
return new DashMediaSource.Factory(
|
return new DashMediaSource.Factory(
|
||||||
getDefaultDashChunkSourceFactory(cachelessDataSourceFactory),
|
getDefaultDashChunkSourceFactory(cachelessDataSourceFactory),
|
||||||
cachelessDataSourceFactory
|
cachelessDataSourceFactory);
|
||||||
)
|
|
||||||
.setLoadErrorHandlingPolicy(
|
|
||||||
new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY));
|
|
||||||
}
|
}
|
||||||
|
//endregion
|
||||||
|
|
||||||
private DefaultDashChunkSource.Factory getDefaultDashChunkSourceFactory(
|
|
||||||
final DataSource.Factory dataSourceFactory
|
|
||||||
) {
|
|
||||||
return new DefaultDashChunkSource.Factory(dataSourceFactory);
|
|
||||||
}
|
|
||||||
|
|
||||||
public HlsMediaSource.Factory getHlsMediaSourceFactory() {
|
//region Generic media source factories
|
||||||
|
public HlsMediaSource.Factory getHlsMediaSourceFactory(
|
||||||
|
@Nullable final NonUriHlsDataSourceFactory.Builder hlsDataSourceFactoryBuilder) {
|
||||||
|
if (hlsDataSourceFactoryBuilder != null) {
|
||||||
|
hlsDataSourceFactoryBuilder.setDataSourceFactory(cacheDataSourceFactory);
|
||||||
|
return new HlsMediaSource.Factory(hlsDataSourceFactoryBuilder.build());
|
||||||
|
}
|
||||||
|
|
||||||
return new HlsMediaSource.Factory(cacheDataSourceFactory);
|
return new HlsMediaSource.Factory(cacheDataSourceFactory);
|
||||||
}
|
}
|
||||||
|
|
||||||
public DashMediaSource.Factory getDashMediaSourceFactory() {
|
public DashMediaSource.Factory getDashMediaSourceFactory() {
|
||||||
return new DashMediaSource.Factory(
|
return new DashMediaSource.Factory(
|
||||||
getDefaultDashChunkSourceFactory(cacheDataSourceFactory),
|
getDefaultDashChunkSourceFactory(cacheDataSourceFactory),
|
||||||
cacheDataSourceFactory
|
cacheDataSourceFactory);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ProgressiveMediaSource.Factory getExtractorMediaSourceFactory() {
|
public ProgressiveMediaSource.Factory getProgressiveMediaSourceFactory() {
|
||||||
return new ProgressiveMediaSource.Factory(cacheDataSourceFactory)
|
return new ProgressiveMediaSource.Factory(cacheDataSourceFactory)
|
||||||
.setContinueLoadingCheckIntervalBytes(continueLoadingCheckIntervalBytes)
|
.setContinueLoadingCheckIntervalBytes(progressiveLoadIntervalBytes);
|
||||||
.setLoadErrorHandlingPolicy(
|
|
||||||
new DefaultLoadErrorHandlingPolicy(EXTRACTOR_MINIMUM_RETRY));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public SingleSampleMediaSource.Factory getSampleMediaSourceFactory() {
|
public SsMediaSource.Factory getSSMediaSourceFactory() {
|
||||||
|
return new SsMediaSource.Factory(
|
||||||
|
new DefaultSsChunkSource.Factory(cachelessDataSourceFactory),
|
||||||
|
cachelessDataSourceFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SingleSampleMediaSource.Factory getSingleSampleMediaSourceFactory() {
|
||||||
return new SingleSampleMediaSource.Factory(cacheDataSourceFactory);
|
return new SingleSampleMediaSource.Factory(cacheDataSourceFactory);
|
||||||
}
|
}
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
|
||||||
|
//region YouTube media source factories
|
||||||
|
public HlsMediaSource.Factory getYoutubeHlsMediaSourceFactory() {
|
||||||
|
return new HlsMediaSource.Factory(ytHlsCacheDataSourceFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DashMediaSource.Factory getYoutubeDashMediaSourceFactory() {
|
||||||
|
return new DashMediaSource.Factory(
|
||||||
|
getDefaultDashChunkSourceFactory(ytDashCacheDataSourceFactory),
|
||||||
|
ytDashCacheDataSourceFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProgressiveMediaSource.Factory getYoutubeProgressiveMediaSourceFactory() {
|
||||||
|
return new ProgressiveMediaSource.Factory(ytProgressiveDashCacheDataSourceFactory)
|
||||||
|
.setContinueLoadingCheckIntervalBytes(progressiveLoadIntervalBytes);
|
||||||
|
}
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
|
||||||
|
//region Static methods
|
||||||
|
private static DefaultDashChunkSource.Factory getDefaultDashChunkSourceFactory(
|
||||||
|
final DataSource.Factory dataSourceFactory) {
|
||||||
|
return new DefaultDashChunkSource.Factory(dataSourceFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static YoutubeHttpDataSource.Factory getYoutubeHttpDataSourceFactory(
|
||||||
|
final boolean rangeParameterEnabled,
|
||||||
|
final boolean rnParameterEnabled) {
|
||||||
|
return new YoutubeHttpDataSource.Factory()
|
||||||
|
.setRangeParameterEnabled(rangeParameterEnabled)
|
||||||
|
.setRnParameterEnabled(rnParameterEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void instantiateCacheIfNeeded(final Context context) {
|
||||||
|
if (cache == null) {
|
||||||
|
final File cacheDir = new File(context.getExternalCacheDir(), CACHE_FOLDER_NAME);
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "instantiateCacheIfNeeded: cacheDir = " + cacheDir.getAbsolutePath());
|
||||||
|
}
|
||||||
|
if (!cacheDir.exists() && !cacheDir.mkdir()) {
|
||||||
|
Log.w(TAG, "instantiateCacheIfNeeded: could not create cache dir");
|
||||||
|
}
|
||||||
|
|
||||||
|
final LeastRecentlyUsedCacheEvictor evictor
|
||||||
|
= new LeastRecentlyUsedCacheEvictor(PlayerHelper.getPreferredCacheSize());
|
||||||
|
cache = new SimpleCache(cacheDir, evictor, new StandaloneDatabaseProvider(context));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//endregion
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,11 +45,9 @@ import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.MediaFormat;
|
import org.schabi.newpipe.extractor.MediaFormat;
|
||||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
||||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
|
||||||
import org.schabi.newpipe.extractor.utils.Utils;
|
import org.schabi.newpipe.extractor.utils.Utils;
|
||||||
import org.schabi.newpipe.player.MainPlayer;
|
import org.schabi.newpipe.player.MainPlayer;
|
||||||
import org.schabi.newpipe.player.Player;
|
import org.schabi.newpipe.player.Player;
|
||||||
|
@ -110,12 +108,14 @@ public final class PlayerHelper {
|
||||||
int MINIMIZE_ON_EXIT_MODE_POPUP = 2;
|
int MINIMIZE_ON_EXIT_MODE_POPUP = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
private PlayerHelper() { }
|
private PlayerHelper() {
|
||||||
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
// Exposed helpers
|
// Exposed helpers
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@NonNull
|
||||||
public static String getTimeString(final int milliSeconds) {
|
public static String getTimeString(final int milliSeconds) {
|
||||||
final int seconds = (milliSeconds % 60000) / 1000;
|
final int seconds = (milliSeconds % 60000) / 1000;
|
||||||
final int minutes = (milliSeconds % 3600000) / 60000;
|
final int minutes = (milliSeconds % 3600000) / 60000;
|
||||||
|
@ -131,15 +131,18 @@ public final class PlayerHelper {
|
||||||
).toString();
|
).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
public static String formatSpeed(final double speed) {
|
public static String formatSpeed(final double speed) {
|
||||||
return SPEED_FORMATTER.format(speed);
|
return SPEED_FORMATTER.format(speed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
public static String formatPitch(final double pitch) {
|
public static String formatPitch(final double pitch) {
|
||||||
return PITCH_FORMATTER.format(pitch);
|
return PITCH_FORMATTER.format(pitch);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String subtitleMimeTypesOf(final MediaFormat format) {
|
@NonNull
|
||||||
|
public static String subtitleMimeTypesOf(@NonNull final MediaFormat format) {
|
||||||
switch (format) {
|
switch (format) {
|
||||||
case VTT:
|
case VTT:
|
||||||
return MimeTypes.TEXT_VTT;
|
return MimeTypes.TEXT_VTT;
|
||||||
|
@ -190,18 +193,6 @@ public final class PlayerHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public static String cacheKeyOf(@NonNull final StreamInfo info,
|
|
||||||
@NonNull final VideoStream video) {
|
|
||||||
return info.getUrl() + video.getResolution() + video.getFormat().getName();
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public static String cacheKeyOf(@NonNull final StreamInfo info,
|
|
||||||
@NonNull final AudioStream audio) {
|
|
||||||
return info.getUrl() + audio.getAverageBitrate() + audio.getFormat().getName();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a {@link StreamInfo} and the existing queue items,
|
* Given a {@link StreamInfo} and the existing queue items,
|
||||||
* provide the {@link SinglePlayQueue} consisting of the next video for auto queueing.
|
* provide the {@link SinglePlayQueue} consisting of the next video for auto queueing.
|
||||||
|
@ -233,7 +224,7 @@ public final class PlayerHelper {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (relatedItems.get(0) != null && relatedItems.get(0) instanceof StreamInfoItem
|
if (relatedItems.get(0) instanceof StreamInfoItem
|
||||||
&& !urls.contains(relatedItems.get(0).getUrl())) {
|
&& !urls.contains(relatedItems.get(0).getUrl())) {
|
||||||
return getAutoQueuedSinglePlayQueue((StreamInfoItem) relatedItems.get(0));
|
return getAutoQueuedSinglePlayQueue((StreamInfoItem) relatedItems.get(0));
|
||||||
}
|
}
|
||||||
|
@ -335,6 +326,7 @@ public final class PlayerHelper {
|
||||||
return 2 * 1024 * 1024L; // ExoPlayer CacheDataSink.MIN_RECOMMENDED_FRAGMENT_SIZE
|
return 2 * 1024 * 1024L; // ExoPlayer CacheDataSink.MIN_RECOMMENDED_FRAGMENT_SIZE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
public static ExoTrackSelection.Factory getQualitySelector() {
|
public static ExoTrackSelection.Factory getQualitySelector() {
|
||||||
return new AdaptiveTrackSelection.Factory(
|
return new AdaptiveTrackSelection.Factory(
|
||||||
1000,
|
1000,
|
||||||
|
@ -389,7 +381,7 @@ public final class PlayerHelper {
|
||||||
/**
|
/**
|
||||||
* @param context the Android context
|
* @param context the Android context
|
||||||
* @return the screen brightness to use. A value less than 0 (the default) means to use the
|
* @return the screen brightness to use. A value less than 0 (the default) means to use the
|
||||||
* preferred screen brightness
|
* preferred screen brightness
|
||||||
*/
|
*/
|
||||||
public static float getScreenBrightness(@NonNull final Context context) {
|
public static float getScreenBrightness(@NonNull final Context context) {
|
||||||
final SharedPreferences sp = getPreferences(context);
|
final SharedPreferences sp = getPreferences(context);
|
||||||
|
@ -480,7 +472,8 @@ public final class PlayerHelper {
|
||||||
return REPEAT_MODE_ONE;
|
return REPEAT_MODE_ONE;
|
||||||
case REPEAT_MODE_ONE:
|
case REPEAT_MODE_ONE:
|
||||||
return REPEAT_MODE_ALL;
|
return REPEAT_MODE_ALL;
|
||||||
case REPEAT_MODE_ALL: default:
|
case REPEAT_MODE_ALL:
|
||||||
|
default:
|
||||||
return REPEAT_MODE_OFF;
|
return REPEAT_MODE_OFF;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -548,7 +541,7 @@ public final class PlayerHelper {
|
||||||
player.getContext().getResources().getDimension(R.dimen.popup_default_width);
|
player.getContext().getResources().getDimension(R.dimen.popup_default_width);
|
||||||
final float popupWidth = popupRememberSizeAndPos
|
final float popupWidth = popupRememberSizeAndPos
|
||||||
? player.getPrefs().getFloat(player.getContext().getString(
|
? player.getPrefs().getFloat(player.getContext().getString(
|
||||||
R.string.popup_saved_width_key), defaultSize)
|
R.string.popup_saved_width_key), defaultSize)
|
||||||
: defaultSize;
|
: defaultSize;
|
||||||
final float popupHeight = getMinimumVideoHeight(popupWidth);
|
final float popupHeight = getMinimumVideoHeight(popupWidth);
|
||||||
|
|
||||||
|
@ -564,10 +557,10 @@ public final class PlayerHelper {
|
||||||
final int centerY = (int) (player.getScreenHeight() / 2f - popupHeight / 2f);
|
final int centerY = (int) (player.getScreenHeight() / 2f - popupHeight / 2f);
|
||||||
popupLayoutParams.x = popupRememberSizeAndPos
|
popupLayoutParams.x = popupRememberSizeAndPos
|
||||||
? player.getPrefs().getInt(player.getContext().getString(
|
? player.getPrefs().getInt(player.getContext().getString(
|
||||||
R.string.popup_saved_x_key), centerX) : centerX;
|
R.string.popup_saved_x_key), centerX) : centerX;
|
||||||
popupLayoutParams.y = popupRememberSizeAndPos
|
popupLayoutParams.y = popupRememberSizeAndPos
|
||||||
? player.getPrefs().getInt(player.getContext().getString(
|
? player.getPrefs().getInt(player.getContext().getString(
|
||||||
R.string.popup_saved_y_key), centerY) : centerY;
|
R.string.popup_saved_y_key), centerY) : centerY;
|
||||||
|
|
||||||
return popupLayoutParams;
|
return popupLayoutParams;
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ class QualityClickListener(
|
||||||
val videoStream = player.selectedVideoStream
|
val videoStream = player.selectedVideoStream
|
||||||
if (videoStream != null) {
|
if (videoStream != null) {
|
||||||
player.binding.qualityTextView.text =
|
player.binding.qualityTextView.text =
|
||||||
MediaFormat.getNameById(videoStream.formatId) + " " + videoStream.resolution
|
MediaFormat.getNameById(videoStream.formatId) + " " + videoStream.getResolution()
|
||||||
}
|
}
|
||||||
|
|
||||||
player.saveWasPlaying()
|
player.saveWasPlaying()
|
||||||
|
|
|
@ -1,22 +1,27 @@
|
||||||
package org.schabi.newpipe.player.resolver;
|
package org.schabi.newpipe.player.resolver;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.util.ListHelper.getNonTorrentStreams;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.MediaFormat;
|
|
||||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.player.helper.PlayerDataSource;
|
import org.schabi.newpipe.player.helper.PlayerDataSource;
|
||||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
|
||||||
import org.schabi.newpipe.player.mediaitem.MediaItemTag;
|
import org.schabi.newpipe.player.mediaitem.MediaItemTag;
|
||||||
import org.schabi.newpipe.player.mediaitem.StreamInfoTag;
|
import org.schabi.newpipe.player.mediaitem.StreamInfoTag;
|
||||||
import org.schabi.newpipe.util.ListHelper;
|
import org.schabi.newpipe.util.ListHelper;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class AudioPlaybackResolver implements PlaybackResolver {
|
public class AudioPlaybackResolver implements PlaybackResolver {
|
||||||
|
private static final String TAG = AudioPlaybackResolver.class.getSimpleName();
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
private final Context context;
|
private final Context context;
|
||||||
@NonNull
|
@NonNull
|
||||||
|
@ -31,19 +36,27 @@ public class AudioPlaybackResolver implements PlaybackResolver {
|
||||||
@Override
|
@Override
|
||||||
@Nullable
|
@Nullable
|
||||||
public MediaSource resolve(@NonNull final StreamInfo info) {
|
public MediaSource resolve(@NonNull final StreamInfo info) {
|
||||||
final MediaSource liveSource = maybeBuildLiveMediaSource(dataSource, info);
|
final MediaSource liveSource = PlaybackResolver.maybeBuildLiveMediaSource(dataSource, info);
|
||||||
if (liveSource != null) {
|
if (liveSource != null) {
|
||||||
return liveSource;
|
return liveSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
final int index = ListHelper.getDefaultAudioFormat(context, info.getAudioStreams());
|
final List<AudioStream> audioStreams = getNonTorrentStreams(info.getAudioStreams());
|
||||||
|
|
||||||
|
final int index = ListHelper.getDefaultAudioFormat(context, audioStreams);
|
||||||
if (index < 0 || index >= info.getAudioStreams().size()) {
|
if (index < 0 || index >= info.getAudioStreams().size()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final AudioStream audio = info.getAudioStreams().get(index);
|
final AudioStream audio = info.getAudioStreams().get(index);
|
||||||
final MediaItemTag tag = StreamInfoTag.of(info);
|
final MediaItemTag tag = StreamInfoTag.of(info);
|
||||||
return buildMediaSource(dataSource, audio.getUrl(), PlayerHelper.cacheKeyOf(info, audio),
|
|
||||||
MediaFormat.getSuffixById(audio.getFormatId()), tag);
|
try {
|
||||||
|
return PlaybackResolver.buildMediaSource(
|
||||||
|
dataSource, audio, info, PlaybackResolver.cacheKeyOf(info, audio), tag);
|
||||||
|
} catch (final ResolverException e) {
|
||||||
|
Log.e(TAG, "Unable to create audio source", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,50 +1,193 @@
|
||||||
package org.schabi.newpipe.player.resolver;
|
package org.schabi.newpipe.player.resolver;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.extractor.stream.AudioStream.UNKNOWN_BITRATE;
|
||||||
|
import static org.schabi.newpipe.extractor.stream.VideoStream.RESOLUTION_UNKNOWN;
|
||||||
|
import static org.schabi.newpipe.player.helper.PlayerDataSource.LIVE_STREAM_EDGE_GAP_MILLIS;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.text.TextUtils;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.MediaItem;
|
import com.google.android.exoplayer2.MediaItem;
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||||
|
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||||
|
import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
|
||||||
|
import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser;
|
||||||
|
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
||||||
|
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
||||||
|
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest;
|
||||||
|
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.MediaFormat;
|
||||||
|
import org.schabi.newpipe.extractor.ServiceList;
|
||||||
|
import org.schabi.newpipe.extractor.services.youtube.ItagItem;
|
||||||
|
import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.CreationException;
|
||||||
|
import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeOtfDashManifestCreator;
|
||||||
|
import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubePostLiveStreamDvrDashManifestCreator;
|
||||||
|
import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeProgressiveDashManifestCreator;
|
||||||
|
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||||
|
import org.schabi.newpipe.extractor.stream.DeliveryMethod;
|
||||||
|
import org.schabi.newpipe.extractor.stream.Stream;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||||
|
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||||
|
import org.schabi.newpipe.player.datasource.NonUriHlsDataSourceFactory;
|
||||||
import org.schabi.newpipe.player.helper.PlayerDataSource;
|
import org.schabi.newpipe.player.helper.PlayerDataSource;
|
||||||
import org.schabi.newpipe.player.mediaitem.MediaItemTag;
|
import org.schabi.newpipe.player.mediaitem.MediaItemTag;
|
||||||
import org.schabi.newpipe.player.mediaitem.StreamInfoTag;
|
import org.schabi.newpipe.player.mediaitem.StreamInfoTag;
|
||||||
import org.schabi.newpipe.util.StreamTypeUtil;
|
import org.schabi.newpipe.util.StreamTypeUtil;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import java.io.ByteArrayInputStream;
|
||||||
import androidx.annotation.Nullable;
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import static org.schabi.newpipe.player.helper.PlayerDataSource.LIVE_STREAM_EDGE_GAP_MILLIS;
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This interface is just a shorthand for {@link Resolver} with {@link StreamInfo} as source and
|
||||||
|
* {@link MediaSource} as product. It contains many static methods that can be used by classes
|
||||||
|
* implementing this interface, and nothing else.
|
||||||
|
*/
|
||||||
public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
|
public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
|
||||||
|
String TAG = PlaybackResolver.class.getSimpleName();
|
||||||
|
|
||||||
|
|
||||||
|
//region Cache key generation
|
||||||
|
private static StringBuilder commonCacheKeyOf(final StreamInfo info,
|
||||||
|
final Stream stream,
|
||||||
|
final boolean resolutionOrBitrateUnknown) {
|
||||||
|
// stream info service id
|
||||||
|
final StringBuilder cacheKey = new StringBuilder(info.getServiceId());
|
||||||
|
|
||||||
|
// stream info id
|
||||||
|
cacheKey.append(" ");
|
||||||
|
cacheKey.append(info.getId());
|
||||||
|
|
||||||
|
// stream id (even if unknown)
|
||||||
|
cacheKey.append(" ");
|
||||||
|
cacheKey.append(stream.getId());
|
||||||
|
|
||||||
|
// mediaFormat (if not null)
|
||||||
|
final MediaFormat mediaFormat = stream.getFormat();
|
||||||
|
if (mediaFormat != null) {
|
||||||
|
cacheKey.append(" ");
|
||||||
|
cacheKey.append(mediaFormat.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
// content (only if other information is missing)
|
||||||
|
// If the media format and the resolution/bitrate are both missing, then we don't have
|
||||||
|
// enough information to distinguish this stream from other streams.
|
||||||
|
// So, only in that case, we use the content (i.e. url or manifest) to differentiate
|
||||||
|
// between streams.
|
||||||
|
// Note that if the content were used even when other information is present, then two
|
||||||
|
// streams with the same stats but with different contents (e.g. because the url was
|
||||||
|
// refreshed) will be considered different (i.e. with a different cacheKey), making the
|
||||||
|
// cache useless.
|
||||||
|
if (resolutionOrBitrateUnknown && mediaFormat == null) {
|
||||||
|
cacheKey.append(" ");
|
||||||
|
cacheKey.append(Objects.hash(stream.getContent(), stream.getManifestUrl()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return cacheKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the cache key of a {@link VideoStream video stream}.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* A cache key is unique to the features of the provided video stream, and when possible
|
||||||
|
* independent of <i>transient</i> parameters (such as the URL of the stream).
|
||||||
|
* This ensures that there are no conflicts, but also that the cache is used as much as
|
||||||
|
* possible: the same cache should be used for two streams which have the same features but
|
||||||
|
* e.g. a different URL, since the URL might have been reloaded in the meantime, but the stream
|
||||||
|
* actually referenced by the URL is still the same.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param info the {@link StreamInfo stream info}, to distinguish between streams with
|
||||||
|
* the same features but coming from different stream infos
|
||||||
|
* @param videoStream the {@link VideoStream video stream} for which the cache key should be
|
||||||
|
* created
|
||||||
|
* @return a key to be used to store the cache of the provided {@link VideoStream video stream}
|
||||||
|
*/
|
||||||
|
static String cacheKeyOf(final StreamInfo info, final VideoStream videoStream) {
|
||||||
|
final boolean resolutionUnknown = videoStream.getResolution().equals(RESOLUTION_UNKNOWN);
|
||||||
|
final StringBuilder cacheKey = commonCacheKeyOf(info, videoStream, resolutionUnknown);
|
||||||
|
|
||||||
|
// resolution (if known)
|
||||||
|
if (!resolutionUnknown) {
|
||||||
|
cacheKey.append(" ");
|
||||||
|
cacheKey.append(videoStream.getResolution());
|
||||||
|
}
|
||||||
|
|
||||||
|
// isVideoOnly
|
||||||
|
cacheKey.append(" ");
|
||||||
|
cacheKey.append(videoStream.isVideoOnly());
|
||||||
|
|
||||||
|
return cacheKey.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the cache key of an audio stream.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* A cache key is unique to the features of the provided {@link AudioStream audio stream}, and
|
||||||
|
* when possible independent of <i>transient</i> parameters (such as the URL of the stream).
|
||||||
|
* This ensures that there are no conflicts, but also that the cache is used as much as
|
||||||
|
* possible: the same cache should be used for two streams which have the same features but
|
||||||
|
* e.g. a different URL, since the URL might have been reloaded in the meantime, but the stream
|
||||||
|
* actually referenced by the URL is still the same.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param info the {@link StreamInfo stream info}, to distinguish between streams with
|
||||||
|
* the same features but coming from different stream infos
|
||||||
|
* @param audioStream the {@link AudioStream audio stream} for which the cache key should be
|
||||||
|
* created
|
||||||
|
* @return a key to be used to store the cache of the provided {@link AudioStream audio stream}
|
||||||
|
*/
|
||||||
|
static String cacheKeyOf(final StreamInfo info, final AudioStream audioStream) {
|
||||||
|
final boolean averageBitrateUnknown = audioStream.getAverageBitrate() == UNKNOWN_BITRATE;
|
||||||
|
final StringBuilder cacheKey = commonCacheKeyOf(info, audioStream, averageBitrateUnknown);
|
||||||
|
|
||||||
|
// averageBitrate (if known)
|
||||||
|
if (!averageBitrateUnknown) {
|
||||||
|
cacheKey.append(" ");
|
||||||
|
cacheKey.append(audioStream.getAverageBitrate());
|
||||||
|
}
|
||||||
|
|
||||||
|
return cacheKey.toString();
|
||||||
|
}
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
|
||||||
|
//region Live media sources
|
||||||
@Nullable
|
@Nullable
|
||||||
default MediaSource maybeBuildLiveMediaSource(@NonNull final PlayerDataSource dataSource,
|
static MediaSource maybeBuildLiveMediaSource(final PlayerDataSource dataSource,
|
||||||
@NonNull final StreamInfo info) {
|
final StreamInfo info) {
|
||||||
final StreamType streamType = info.getStreamType();
|
if (!StreamTypeUtil.isLiveStream(info.getStreamType())) {
|
||||||
if (!StreamTypeUtil.isLiveStream(streamType)) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final StreamInfoTag tag = StreamInfoTag.of(info);
|
try {
|
||||||
if (!info.getHlsUrl().isEmpty()) {
|
final StreamInfoTag tag = StreamInfoTag.of(info);
|
||||||
return buildLiveMediaSource(dataSource, info.getHlsUrl(), C.TYPE_HLS, tag);
|
if (!info.getHlsUrl().isEmpty()) {
|
||||||
} else if (!info.getDashMpdUrl().isEmpty()) {
|
return buildLiveMediaSource(dataSource, info.getHlsUrl(), C.TYPE_HLS, tag);
|
||||||
return buildLiveMediaSource(dataSource, info.getDashMpdUrl(), C.TYPE_DASH, tag);
|
} else if (!info.getDashMpdUrl().isEmpty()) {
|
||||||
|
return buildLiveMediaSource(dataSource, info.getDashMpdUrl(), C.TYPE_DASH, tag);
|
||||||
|
}
|
||||||
|
} catch (final Exception e) {
|
||||||
|
Log.w(TAG, "Error when generating live media source, falling back to standard sources",
|
||||||
|
e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
static MediaSource buildLiveMediaSource(final PlayerDataSource dataSource,
|
||||||
default MediaSource buildLiveMediaSource(@NonNull final PlayerDataSource dataSource,
|
final String sourceUrl,
|
||||||
@NonNull final String sourceUrl,
|
@C.ContentType final int type,
|
||||||
@C.ContentType final int type,
|
final MediaItemTag metadata) throws ResolverException {
|
||||||
@NonNull final MediaItemTag metadata) {
|
|
||||||
final MediaSource.Factory factory;
|
final MediaSource.Factory factory;
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case C.TYPE_SS:
|
case C.TYPE_SS:
|
||||||
|
@ -56,8 +199,10 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
|
||||||
case C.TYPE_HLS:
|
case C.TYPE_HLS:
|
||||||
factory = dataSource.getLiveHlsMediaSourceFactory();
|
factory = dataSource.getLiveHlsMediaSourceFactory();
|
||||||
break;
|
break;
|
||||||
|
case C.TYPE_OTHER:
|
||||||
|
case C.TYPE_RTSP:
|
||||||
default:
|
default:
|
||||||
throw new IllegalStateException("Unsupported type: " + type);
|
throw new ResolverException("Unsupported type: " + type);
|
||||||
}
|
}
|
||||||
|
|
||||||
return factory.createMediaSource(
|
return factory.createMediaSource(
|
||||||
|
@ -67,46 +212,317 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
|
||||||
.setLiveConfiguration(
|
.setLiveConfiguration(
|
||||||
new MediaItem.LiveConfiguration.Builder()
|
new MediaItem.LiveConfiguration.Builder()
|
||||||
.setTargetOffsetMs(LIVE_STREAM_EDGE_GAP_MILLIS)
|
.setTargetOffsetMs(LIVE_STREAM_EDGE_GAP_MILLIS)
|
||||||
.build()
|
.build())
|
||||||
)
|
.build());
|
||||||
.build()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
//endregion
|
||||||
|
|
||||||
@NonNull
|
|
||||||
default MediaSource buildMediaSource(@NonNull final PlayerDataSource dataSource,
|
|
||||||
@NonNull final String sourceUrl,
|
|
||||||
@NonNull final String cacheKey,
|
|
||||||
@NonNull final String overrideExtension,
|
|
||||||
@NonNull final MediaItemTag metadata) {
|
|
||||||
final Uri uri = Uri.parse(sourceUrl);
|
|
||||||
@C.ContentType final int type = TextUtils.isEmpty(overrideExtension)
|
|
||||||
? Util.inferContentType(uri) : Util.inferContentType("." + overrideExtension);
|
|
||||||
|
|
||||||
final MediaSource.Factory factory;
|
//region Generic media sources
|
||||||
switch (type) {
|
static MediaSource buildMediaSource(final PlayerDataSource dataSource,
|
||||||
case C.TYPE_SS:
|
final Stream stream,
|
||||||
factory = dataSource.getLiveSsMediaSourceFactory();
|
final StreamInfo streamInfo,
|
||||||
break;
|
final String cacheKey,
|
||||||
case C.TYPE_DASH:
|
final MediaItemTag metadata) throws ResolverException {
|
||||||
factory = dataSource.getDashMediaSourceFactory();
|
if (streamInfo.getService() == ServiceList.YouTube) {
|
||||||
break;
|
return createYoutubeMediaSource(stream, streamInfo, dataSource, cacheKey, metadata);
|
||||||
case C.TYPE_HLS:
|
|
||||||
factory = dataSource.getHlsMediaSourceFactory();
|
|
||||||
break;
|
|
||||||
case C.TYPE_OTHER:
|
|
||||||
factory = dataSource.getExtractorMediaSourceFactory();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new IllegalStateException("Unsupported type: " + type);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return factory.createMediaSource(
|
final DeliveryMethod deliveryMethod = stream.getDeliveryMethod();
|
||||||
new MediaItem.Builder()
|
switch (deliveryMethod) {
|
||||||
.setTag(metadata)
|
case PROGRESSIVE_HTTP:
|
||||||
.setUri(uri)
|
return buildProgressiveMediaSource(dataSource, stream, cacheKey, metadata);
|
||||||
.setCustomCacheKey(cacheKey)
|
case DASH:
|
||||||
.build()
|
return buildDashMediaSource(dataSource, stream, cacheKey, metadata);
|
||||||
);
|
case HLS:
|
||||||
|
return buildHlsMediaSource(dataSource, stream, cacheKey, metadata);
|
||||||
|
case SS:
|
||||||
|
return buildSSMediaSource(dataSource, stream, cacheKey, metadata);
|
||||||
|
// Torrent streams are not supported by ExoPlayer
|
||||||
|
default:
|
||||||
|
throw new ResolverException("Unsupported delivery type: " + deliveryMethod);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static ProgressiveMediaSource buildProgressiveMediaSource(
|
||||||
|
final PlayerDataSource dataSource,
|
||||||
|
final Stream stream,
|
||||||
|
final String cacheKey,
|
||||||
|
final MediaItemTag metadata) throws ResolverException {
|
||||||
|
if (!stream.isUrl()) {
|
||||||
|
throw new ResolverException("Non URI progressive contents are not supported");
|
||||||
|
}
|
||||||
|
throwResolverExceptionIfUrlNullOrEmpty(stream.getContent());
|
||||||
|
return dataSource.getProgressiveMediaSourceFactory().createMediaSource(
|
||||||
|
new MediaItem.Builder()
|
||||||
|
.setTag(metadata)
|
||||||
|
.setUri(Uri.parse(stream.getContent()))
|
||||||
|
.setCustomCacheKey(cacheKey)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DashMediaSource buildDashMediaSource(final PlayerDataSource dataSource,
|
||||||
|
final Stream stream,
|
||||||
|
final String cacheKey,
|
||||||
|
final MediaItemTag metadata)
|
||||||
|
throws ResolverException {
|
||||||
|
|
||||||
|
if (stream.isUrl()) {
|
||||||
|
throwResolverExceptionIfUrlNullOrEmpty(stream.getContent());
|
||||||
|
return dataSource.getDashMediaSourceFactory().createMediaSource(
|
||||||
|
new MediaItem.Builder()
|
||||||
|
.setTag(metadata)
|
||||||
|
.setUri(Uri.parse(stream.getContent()))
|
||||||
|
.setCustomCacheKey(cacheKey)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return dataSource.getDashMediaSourceFactory().createMediaSource(
|
||||||
|
createDashManifest(stream.getContent(), stream),
|
||||||
|
new MediaItem.Builder()
|
||||||
|
.setTag(metadata)
|
||||||
|
.setUri(manifestUrlToUri(stream.getManifestUrl()))
|
||||||
|
.setCustomCacheKey(cacheKey)
|
||||||
|
.build());
|
||||||
|
} catch (final IOException e) {
|
||||||
|
throw new ResolverException(
|
||||||
|
"Could not create a DASH media source/manifest from the manifest text", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DashManifest createDashManifest(final String manifestContent,
|
||||||
|
final Stream stream) throws IOException {
|
||||||
|
return new DashManifestParser().parse(manifestUrlToUri(stream.getManifestUrl()),
|
||||||
|
new ByteArrayInputStream(manifestContent.getBytes(StandardCharsets.UTF_8)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HlsMediaSource buildHlsMediaSource(final PlayerDataSource dataSource,
|
||||||
|
final Stream stream,
|
||||||
|
final String cacheKey,
|
||||||
|
final MediaItemTag metadata)
|
||||||
|
throws ResolverException {
|
||||||
|
if (stream.isUrl()) {
|
||||||
|
throwResolverExceptionIfUrlNullOrEmpty(stream.getContent());
|
||||||
|
return dataSource.getHlsMediaSourceFactory(null).createMediaSource(
|
||||||
|
new MediaItem.Builder()
|
||||||
|
.setTag(metadata)
|
||||||
|
.setUri(Uri.parse(stream.getContent()))
|
||||||
|
.setCustomCacheKey(cacheKey)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
final NonUriHlsDataSourceFactory.Builder hlsDataSourceFactoryBuilder =
|
||||||
|
new NonUriHlsDataSourceFactory.Builder();
|
||||||
|
hlsDataSourceFactoryBuilder.setPlaylistString(stream.getContent());
|
||||||
|
|
||||||
|
return dataSource.getHlsMediaSourceFactory(hlsDataSourceFactoryBuilder)
|
||||||
|
.createMediaSource(new MediaItem.Builder()
|
||||||
|
.setTag(metadata)
|
||||||
|
.setUri(manifestUrlToUri(stream.getManifestUrl()))
|
||||||
|
.setCustomCacheKey(cacheKey)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SsMediaSource buildSSMediaSource(final PlayerDataSource dataSource,
|
||||||
|
final Stream stream,
|
||||||
|
final String cacheKey,
|
||||||
|
final MediaItemTag metadata)
|
||||||
|
throws ResolverException {
|
||||||
|
if (stream.isUrl()) {
|
||||||
|
throwResolverExceptionIfUrlNullOrEmpty(stream.getContent());
|
||||||
|
return dataSource.getSSMediaSourceFactory().createMediaSource(
|
||||||
|
new MediaItem.Builder()
|
||||||
|
.setTag(metadata)
|
||||||
|
.setUri(Uri.parse(stream.getContent()))
|
||||||
|
.setCustomCacheKey(cacheKey)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
final Uri manifestUri = manifestUrlToUri(stream.getManifestUrl());
|
||||||
|
|
||||||
|
final SsManifest smoothStreamingManifest;
|
||||||
|
try {
|
||||||
|
final ByteArrayInputStream smoothStreamingManifestInput = new ByteArrayInputStream(
|
||||||
|
stream.getContent().getBytes(StandardCharsets.UTF_8));
|
||||||
|
smoothStreamingManifest = new SsManifestParser().parse(manifestUri,
|
||||||
|
smoothStreamingManifestInput);
|
||||||
|
} catch (final IOException e) {
|
||||||
|
throw new ResolverException("Error when parsing manual SS manifest", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return dataSource.getSSMediaSourceFactory().createMediaSource(
|
||||||
|
smoothStreamingManifest,
|
||||||
|
new MediaItem.Builder()
|
||||||
|
.setTag(metadata)
|
||||||
|
.setUri(manifestUri)
|
||||||
|
.setCustomCacheKey(cacheKey)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
|
||||||
|
//region YouTube media sources
|
||||||
|
private static MediaSource createYoutubeMediaSource(final Stream stream,
|
||||||
|
final StreamInfo streamInfo,
|
||||||
|
final PlayerDataSource dataSource,
|
||||||
|
final String cacheKey,
|
||||||
|
final MediaItemTag metadata)
|
||||||
|
throws ResolverException {
|
||||||
|
if (!(stream instanceof AudioStream || stream instanceof VideoStream)) {
|
||||||
|
throw new ResolverException("Generation of YouTube DASH manifest for "
|
||||||
|
+ stream.getClass().getSimpleName() + " is not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
final StreamType streamType = streamInfo.getStreamType();
|
||||||
|
if (streamType == StreamType.VIDEO_STREAM) {
|
||||||
|
return createYoutubeMediaSourceOfVideoStreamType(dataSource, stream, streamInfo,
|
||||||
|
cacheKey, metadata);
|
||||||
|
} else if (streamType == StreamType.POST_LIVE_STREAM) {
|
||||||
|
// If the content is not an URL, uses the DASH delivery method and if the stream type
|
||||||
|
// of the stream is a post live stream, it means that the content is an ended
|
||||||
|
// livestream so we need to generate the manifest corresponding to the content
|
||||||
|
// (which is the last segment of the stream)
|
||||||
|
|
||||||
|
try {
|
||||||
|
final ItagItem itagItem = Objects.requireNonNull(stream.getItagItem());
|
||||||
|
final String manifestString = YoutubePostLiveStreamDvrDashManifestCreator
|
||||||
|
.fromPostLiveStreamDvrStreamingUrl(stream.getContent(),
|
||||||
|
itagItem,
|
||||||
|
itagItem.getTargetDurationSec(),
|
||||||
|
streamInfo.getDuration());
|
||||||
|
return buildYoutubeManualDashMediaSource(dataSource,
|
||||||
|
createDashManifest(manifestString, stream), stream, cacheKey,
|
||||||
|
metadata);
|
||||||
|
} catch (final CreationException | IOException | NullPointerException e) {
|
||||||
|
throw new ResolverException(
|
||||||
|
"Error when generating the DASH manifest of YouTube ended live stream", e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new ResolverException(
|
||||||
|
"DASH manifest generation of YouTube livestreams is not supported");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MediaSource createYoutubeMediaSourceOfVideoStreamType(
|
||||||
|
final PlayerDataSource dataSource,
|
||||||
|
final Stream stream,
|
||||||
|
final StreamInfo streamInfo,
|
||||||
|
final String cacheKey,
|
||||||
|
final MediaItemTag metadata) throws ResolverException {
|
||||||
|
final DeliveryMethod deliveryMethod = stream.getDeliveryMethod();
|
||||||
|
switch (deliveryMethod) {
|
||||||
|
case PROGRESSIVE_HTTP:
|
||||||
|
if ((stream instanceof VideoStream && ((VideoStream) stream).isVideoOnly())
|
||||||
|
|| stream instanceof AudioStream) {
|
||||||
|
try {
|
||||||
|
final String manifestString = YoutubeProgressiveDashManifestCreator
|
||||||
|
.fromProgressiveStreamingUrl(stream.getContent(),
|
||||||
|
Objects.requireNonNull(stream.getItagItem()),
|
||||||
|
streamInfo.getDuration());
|
||||||
|
return buildYoutubeManualDashMediaSource(dataSource,
|
||||||
|
createDashManifest(manifestString, stream), stream, cacheKey,
|
||||||
|
metadata);
|
||||||
|
} catch (final CreationException | IOException | NullPointerException e) {
|
||||||
|
Log.w(TAG, "Error when generating or parsing DASH manifest of "
|
||||||
|
+ "YouTube progressive stream, falling back to a "
|
||||||
|
+ "ProgressiveMediaSource.", e);
|
||||||
|
return buildYoutubeProgressiveMediaSource(dataSource, stream, cacheKey,
|
||||||
|
metadata);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Legacy progressive streams, subtitles are handled by
|
||||||
|
// VideoPlaybackResolver
|
||||||
|
return buildYoutubeProgressiveMediaSource(dataSource, stream, cacheKey,
|
||||||
|
metadata);
|
||||||
|
}
|
||||||
|
case DASH:
|
||||||
|
// If the content is not a URL, uses the DASH delivery method and if the stream
|
||||||
|
// type of the stream is a video stream, it means the content is an OTF stream
|
||||||
|
// so we need to generate the manifest corresponding to the content (which is
|
||||||
|
// the base URL of the OTF stream).
|
||||||
|
|
||||||
|
try {
|
||||||
|
final String manifestString = YoutubeOtfDashManifestCreator
|
||||||
|
.fromOtfStreamingUrl(stream.getContent(),
|
||||||
|
Objects.requireNonNull(stream.getItagItem()),
|
||||||
|
streamInfo.getDuration());
|
||||||
|
return buildYoutubeManualDashMediaSource(dataSource,
|
||||||
|
createDashManifest(manifestString, stream), stream, cacheKey,
|
||||||
|
metadata);
|
||||||
|
} catch (final CreationException | IOException | NullPointerException e) {
|
||||||
|
Log.e(TAG,
|
||||||
|
"Error when generating the DASH manifest of YouTube OTF stream", e);
|
||||||
|
throw new ResolverException(
|
||||||
|
"Error when generating the DASH manifest of YouTube OTF stream", e);
|
||||||
|
}
|
||||||
|
case HLS:
|
||||||
|
return dataSource.getYoutubeHlsMediaSourceFactory().createMediaSource(
|
||||||
|
new MediaItem.Builder()
|
||||||
|
.setTag(metadata)
|
||||||
|
.setUri(Uri.parse(stream.getContent()))
|
||||||
|
.setCustomCacheKey(cacheKey)
|
||||||
|
.build());
|
||||||
|
default:
|
||||||
|
throw new ResolverException("Unsupported delivery method for YouTube contents: "
|
||||||
|
+ deliveryMethod);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DashMediaSource buildYoutubeManualDashMediaSource(
|
||||||
|
final PlayerDataSource dataSource,
|
||||||
|
final DashManifest dashManifest,
|
||||||
|
final Stream stream,
|
||||||
|
final String cacheKey,
|
||||||
|
final MediaItemTag metadata) {
|
||||||
|
return dataSource.getYoutubeDashMediaSourceFactory().createMediaSource(dashManifest,
|
||||||
|
new MediaItem.Builder()
|
||||||
|
.setTag(metadata)
|
||||||
|
.setUri(Uri.parse(stream.getContent()))
|
||||||
|
.setCustomCacheKey(cacheKey)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ProgressiveMediaSource buildYoutubeProgressiveMediaSource(
|
||||||
|
final PlayerDataSource dataSource,
|
||||||
|
final Stream stream,
|
||||||
|
final String cacheKey,
|
||||||
|
final MediaItemTag metadata) {
|
||||||
|
return dataSource.getYoutubeProgressiveMediaSourceFactory()
|
||||||
|
.createMediaSource(new MediaItem.Builder()
|
||||||
|
.setTag(metadata)
|
||||||
|
.setUri(Uri.parse(stream.getContent()))
|
||||||
|
.setCustomCacheKey(cacheKey)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
|
||||||
|
//region Utils
|
||||||
|
private static Uri manifestUrlToUri(final String manifestUrl) {
|
||||||
|
return Uri.parse(Objects.requireNonNullElse(manifestUrl, ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void throwResolverExceptionIfUrlNullOrEmpty(@Nullable final String url)
|
||||||
|
throws ResolverException {
|
||||||
|
if (url == null) {
|
||||||
|
throw new ResolverException("Null stream URL");
|
||||||
|
} else if (url.isEmpty()) {
|
||||||
|
throw new ResolverException("Empty stream URL");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
|
||||||
|
//region Resolver exception
|
||||||
|
final class ResolverException extends Exception {
|
||||||
|
public ResolverException(final String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResolverException(final String message, final Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//endregion
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package org.schabi.newpipe.player.resolver;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
@ -27,8 +28,12 @@ import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
import static com.google.android.exoplayer2.C.TIME_UNSET;
|
import static com.google.android.exoplayer2.C.TIME_UNSET;
|
||||||
|
import static org.schabi.newpipe.util.ListHelper.getUrlAndNonTorrentStreams;
|
||||||
|
import static org.schabi.newpipe.util.ListHelper.getNonTorrentStreams;
|
||||||
|
|
||||||
public class VideoPlaybackResolver implements PlaybackResolver {
|
public class VideoPlaybackResolver implements PlaybackResolver {
|
||||||
|
private static final String TAG = VideoPlaybackResolver.class.getSimpleName();
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
private final Context context;
|
private final Context context;
|
||||||
@NonNull
|
@NonNull
|
||||||
|
@ -57,7 +62,7 @@ public class VideoPlaybackResolver implements PlaybackResolver {
|
||||||
@Override
|
@Override
|
||||||
@Nullable
|
@Nullable
|
||||||
public MediaSource resolve(@NonNull final StreamInfo info) {
|
public MediaSource resolve(@NonNull final StreamInfo info) {
|
||||||
final MediaSource liveSource = maybeBuildLiveMediaSource(dataSource, info);
|
final MediaSource liveSource = PlaybackResolver.maybeBuildLiveMediaSource(dataSource, info);
|
||||||
if (liveSource != null) {
|
if (liveSource != null) {
|
||||||
streamSourceType = SourceType.LIVE_STREAM;
|
streamSourceType = SourceType.LIVE_STREAM;
|
||||||
return liveSource;
|
return liveSource;
|
||||||
|
@ -66,40 +71,51 @@ public class VideoPlaybackResolver implements PlaybackResolver {
|
||||||
final List<MediaSource> mediaSources = new ArrayList<>();
|
final List<MediaSource> mediaSources = new ArrayList<>();
|
||||||
|
|
||||||
// Create video stream source
|
// Create video stream source
|
||||||
final List<VideoStream> videos = ListHelper.getSortedStreamVideosList(context,
|
final List<VideoStream> videoStreamsList = ListHelper.getSortedStreamVideosList(context,
|
||||||
info.getVideoStreams(), info.getVideoOnlyStreams(), false, true);
|
getNonTorrentStreams(info.getVideoStreams()),
|
||||||
|
getNonTorrentStreams(info.getVideoOnlyStreams()), false, true);
|
||||||
final int index;
|
final int index;
|
||||||
if (videos.isEmpty()) {
|
if (videoStreamsList.isEmpty()) {
|
||||||
index = -1;
|
index = -1;
|
||||||
} else if (playbackQuality == null) {
|
} else if (playbackQuality == null) {
|
||||||
index = qualityResolver.getDefaultResolutionIndex(videos);
|
index = qualityResolver.getDefaultResolutionIndex(videoStreamsList);
|
||||||
} else {
|
} else {
|
||||||
index = qualityResolver.getOverrideResolutionIndex(videos, getPlaybackQuality());
|
index = qualityResolver.getOverrideResolutionIndex(videoStreamsList,
|
||||||
|
getPlaybackQuality());
|
||||||
}
|
}
|
||||||
final MediaItemTag tag = StreamInfoTag.of(info, videos, index);
|
final MediaItemTag tag = StreamInfoTag.of(info, videoStreamsList, index);
|
||||||
@Nullable final VideoStream video = tag.getMaybeQuality()
|
@Nullable final VideoStream video = tag.getMaybeQuality()
|
||||||
.map(MediaItemTag.Quality::getSelectedVideoStream)
|
.map(MediaItemTag.Quality::getSelectedVideoStream)
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
|
|
||||||
if (video != null) {
|
if (video != null) {
|
||||||
final MediaSource streamSource = buildMediaSource(dataSource, video.getUrl(),
|
try {
|
||||||
PlayerHelper.cacheKeyOf(info, video),
|
final MediaSource streamSource = PlaybackResolver.buildMediaSource(
|
||||||
MediaFormat.getSuffixById(video.getFormatId()), tag);
|
dataSource, video, info, PlaybackResolver.cacheKeyOf(info, video), tag);
|
||||||
mediaSources.add(streamSource);
|
mediaSources.add(streamSource);
|
||||||
|
} catch (final ResolverException e) {
|
||||||
|
Log.e(TAG, "Unable to create video source", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create optional audio stream source
|
// Create optional audio stream source
|
||||||
final List<AudioStream> audioStreams = info.getAudioStreams();
|
final List<AudioStream> audioStreams = getNonTorrentStreams(info.getAudioStreams());
|
||||||
final AudioStream audio = audioStreams.isEmpty() ? null : audioStreams.get(
|
final AudioStream audio = audioStreams.isEmpty() ? null : audioStreams.get(
|
||||||
ListHelper.getDefaultAudioFormat(context, audioStreams));
|
ListHelper.getDefaultAudioFormat(context, audioStreams));
|
||||||
|
|
||||||
// Use the audio stream if there is no video stream, or
|
// Use the audio stream if there is no video stream, or
|
||||||
// Merge with audio stream in case if video does not contain audio
|
// merge with audio stream in case if video does not contain audio
|
||||||
if (audio != null && (video == null || video.isVideoOnly)) {
|
if (audio != null && (video == null || video.isVideoOnly())) {
|
||||||
final MediaSource audioSource = buildMediaSource(dataSource, audio.getUrl(),
|
try {
|
||||||
PlayerHelper.cacheKeyOf(info, audio),
|
final MediaSource audioSource = PlaybackResolver.buildMediaSource(
|
||||||
MediaFormat.getSuffixById(audio.getFormatId()), tag);
|
dataSource, audio, info, PlaybackResolver.cacheKeyOf(info, audio), tag);
|
||||||
mediaSources.add(audioSource);
|
mediaSources.add(audioSource);
|
||||||
streamSourceType = SourceType.VIDEO_WITH_SEPARATED_AUDIO;
|
streamSourceType = SourceType.VIDEO_WITH_SEPARATED_AUDIO;
|
||||||
|
} catch (final ResolverException e) {
|
||||||
|
Log.e(TAG, "Unable to create audio source", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
streamSourceType = SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY;
|
streamSourceType = SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY;
|
||||||
}
|
}
|
||||||
|
@ -108,36 +124,39 @@ public class VideoPlaybackResolver implements PlaybackResolver {
|
||||||
if (mediaSources.isEmpty()) {
|
if (mediaSources.isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Below are auxiliary media sources
|
// Below are auxiliary media sources
|
||||||
|
|
||||||
// Create subtitle sources
|
// Create subtitle sources
|
||||||
if (info.getSubtitles() != null) {
|
final List<SubtitlesStream> subtitlesStreams = info.getSubtitles();
|
||||||
for (final SubtitlesStream subtitle : info.getSubtitles()) {
|
if (subtitlesStreams != null) {
|
||||||
final String mimeType = PlayerHelper.subtitleMimeTypesOf(subtitle.getFormat());
|
// Torrent and non URL subtitles are not supported by ExoPlayer
|
||||||
if (mimeType == null) {
|
final List<SubtitlesStream> nonTorrentAndUrlStreams = getUrlAndNonTorrentStreams(
|
||||||
continue;
|
subtitlesStreams);
|
||||||
|
for (final SubtitlesStream subtitle : nonTorrentAndUrlStreams) {
|
||||||
|
final MediaFormat mediaFormat = subtitle.getFormat();
|
||||||
|
if (mediaFormat != null) {
|
||||||
|
@C.RoleFlags final int textRoleFlag = subtitle.isAutoGenerated()
|
||||||
|
? C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND
|
||||||
|
: C.ROLE_FLAG_CAPTION;
|
||||||
|
final MediaItem.SubtitleConfiguration textMediaItem =
|
||||||
|
new MediaItem.SubtitleConfiguration.Builder(
|
||||||
|
Uri.parse(subtitle.getContent()))
|
||||||
|
.setMimeType(mediaFormat.getMimeType())
|
||||||
|
.setRoleFlags(textRoleFlag)
|
||||||
|
.setLanguage(PlayerHelper.captionLanguageOf(context, subtitle))
|
||||||
|
.build();
|
||||||
|
final MediaSource textSource = dataSource.getSingleSampleMediaSourceFactory()
|
||||||
|
.createMediaSource(textMediaItem, TIME_UNSET);
|
||||||
|
mediaSources.add(textSource);
|
||||||
}
|
}
|
||||||
final @C.RoleFlags int textRoleFlag = subtitle.isAutoGenerated()
|
|
||||||
? C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND
|
|
||||||
: C.ROLE_FLAG_CAPTION;
|
|
||||||
final MediaItem.SubtitleConfiguration textMediaItem =
|
|
||||||
new MediaItem.SubtitleConfiguration.Builder(Uri.parse(subtitle.getUrl()))
|
|
||||||
.setMimeType(mimeType)
|
|
||||||
.setRoleFlags(textRoleFlag)
|
|
||||||
.setLanguage(PlayerHelper.captionLanguageOf(context, subtitle))
|
|
||||||
.build();
|
|
||||||
final MediaSource textSource = dataSource
|
|
||||||
.getSampleMediaSourceFactory()
|
|
||||||
.createMediaSource(textMediaItem, TIME_UNSET);
|
|
||||||
mediaSources.add(textSource);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mediaSources.size() == 1) {
|
if (mediaSources.size() == 1) {
|
||||||
return mediaSources.get(0);
|
return mediaSources.get(0);
|
||||||
} else {
|
} else {
|
||||||
return new MergingMediaSource(mediaSources.toArray(
|
return new MergingMediaSource(true, mediaSources.toArray(new MediaSource[0]));
|
||||||
new MediaSource[0]));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,8 @@ import androidx.preference.PreferenceManager;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.extractor.MediaFormat;
|
import org.schabi.newpipe.extractor.MediaFormat;
|
||||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||||
|
import org.schabi.newpipe.extractor.stream.DeliveryMethod;
|
||||||
|
import org.schabi.newpipe.extractor.stream.Stream;
|
||||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -24,6 +26,7 @@ import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.function.Predicate;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public final class ListHelper {
|
public final class ListHelper {
|
||||||
|
@ -37,10 +40,9 @@ public final class ListHelper {
|
||||||
// Audio format in order of efficiency. 0=most efficient, n=least efficient
|
// Audio format in order of efficiency. 0=most efficient, n=least efficient
|
||||||
private static final List<MediaFormat> AUDIO_FORMAT_EFFICIENCY_RANKING =
|
private static final List<MediaFormat> AUDIO_FORMAT_EFFICIENCY_RANKING =
|
||||||
Arrays.asList(MediaFormat.WEBMA, MediaFormat.M4A, MediaFormat.MP3);
|
Arrays.asList(MediaFormat.WEBMA, MediaFormat.M4A, MediaFormat.MP3);
|
||||||
|
// Use a HashSet for better performance
|
||||||
private static final Set<String> HIGH_RESOLUTION_LIST
|
private static final Set<String> HIGH_RESOLUTION_LIST = new HashSet<>(
|
||||||
// Uses a HashSet for better performance
|
Arrays.asList("1440p", "2160p"));
|
||||||
= new HashSet<>(Arrays.asList("1440p", "2160p", "1440p60", "2160p60"));
|
|
||||||
|
|
||||||
private ListHelper() { }
|
private ListHelper() { }
|
||||||
|
|
||||||
|
@ -110,6 +112,51 @@ public final class ListHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a {@link Stream} list which uses the given delivery method from a {@link Stream}
|
||||||
|
* list.
|
||||||
|
*
|
||||||
|
* @param streamList the original {@link Stream stream} list
|
||||||
|
* @param deliveryMethod the {@link DeliveryMethod delivery method}
|
||||||
|
* @param <S> the item type's class that extends {@link Stream}
|
||||||
|
* @return a {@link Stream stream} list which uses the given delivery method
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public static <S extends Stream> List<S> getStreamsOfSpecifiedDelivery(
|
||||||
|
final List<S> streamList,
|
||||||
|
final DeliveryMethod deliveryMethod) {
|
||||||
|
return getFilteredStreamList(streamList,
|
||||||
|
stream -> stream.getDeliveryMethod() == deliveryMethod);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a {@link Stream} list which only contains URL streams and non-torrent streams.
|
||||||
|
*
|
||||||
|
* @param streamList the original stream list
|
||||||
|
* @param <S> the item type's class that extends {@link Stream}
|
||||||
|
* @return a stream list which only contains URL streams and non-torrent streams
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public static <S extends Stream> List<S> getUrlAndNonTorrentStreams(
|
||||||
|
final List<S> streamList) {
|
||||||
|
return getFilteredStreamList(streamList,
|
||||||
|
stream -> stream.isUrl() && stream.getDeliveryMethod() != DeliveryMethod.TORRENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a {@link Stream} list which only contains non-torrent streams.
|
||||||
|
*
|
||||||
|
* @param streamList the original stream list
|
||||||
|
* @param <S> the item type's class that extends {@link Stream}
|
||||||
|
* @return a stream list which only contains non-torrent streams
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public static <S extends Stream> List<S> getNonTorrentStreams(
|
||||||
|
final List<S> streamList) {
|
||||||
|
return getFilteredStreamList(streamList,
|
||||||
|
stream -> stream.getDeliveryMethod() != DeliveryMethod.TORRENT);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Join the two lists of video streams (video_only and normal videos),
|
* Join the two lists of video streams (video_only and normal videos),
|
||||||
* and sort them according with default format chosen by the user.
|
* and sort them according with default format chosen by the user.
|
||||||
|
@ -145,6 +192,26 @@ public final class ListHelper {
|
||||||
// Utils
|
// Utils
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a filtered stream list, by using Java 8 Stream's API and the given predicate.
|
||||||
|
*
|
||||||
|
* @param streamList the stream list to filter
|
||||||
|
* @param streamListPredicate the predicate which will be used to filter streams
|
||||||
|
* @param <S> the item type's class that extends {@link Stream}
|
||||||
|
* @return a new stream list filtered using the given predicate
|
||||||
|
*/
|
||||||
|
private static <S extends Stream> List<S> getFilteredStreamList(
|
||||||
|
final List<S> streamList,
|
||||||
|
final Predicate<S> streamListPredicate) {
|
||||||
|
if (streamList == null) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return streamList.stream()
|
||||||
|
.filter(streamListPredicate)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
private static String computeDefaultResolution(final Context context, final int key,
|
private static String computeDefaultResolution(final Context context, final int key,
|
||||||
final int value) {
|
final int value) {
|
||||||
final SharedPreferences preferences
|
final SharedPreferences preferences
|
||||||
|
@ -177,7 +244,7 @@ public final class ListHelper {
|
||||||
static int getDefaultResolutionIndex(final String defaultResolution,
|
static int getDefaultResolutionIndex(final String defaultResolution,
|
||||||
final String bestResolutionKey,
|
final String bestResolutionKey,
|
||||||
final MediaFormat defaultFormat,
|
final MediaFormat defaultFormat,
|
||||||
final List<VideoStream> videoStreams) {
|
@Nullable final List<VideoStream> videoStreams) {
|
||||||
if (videoStreams == null || videoStreams.isEmpty()) {
|
if (videoStreams == null || videoStreams.isEmpty()) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
@ -233,7 +300,9 @@ public final class ListHelper {
|
||||||
.flatMap(List::stream)
|
.flatMap(List::stream)
|
||||||
// Filter out higher resolutions (or not if high resolutions should always be shown)
|
// Filter out higher resolutions (or not if high resolutions should always be shown)
|
||||||
.filter(stream -> showHigherResolutions
|
.filter(stream -> showHigherResolutions
|
||||||
|| !HIGH_RESOLUTION_LIST.contains(stream.getResolution()))
|
|| !HIGH_RESOLUTION_LIST.contains(stream.getResolution()
|
||||||
|
// Replace any frame rate with nothing
|
||||||
|
.replaceAll("p\\d+$", "p")))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
final HashMap<String, VideoStream> hashMap = new HashMap<>();
|
final HashMap<String, VideoStream> hashMap = new HashMap<>();
|
||||||
|
@ -366,8 +435,9 @@ public final class ListHelper {
|
||||||
* @param videoStreams the available video streams
|
* @param videoStreams the available video streams
|
||||||
* @return the index of the preferred video stream
|
* @return the index of the preferred video stream
|
||||||
*/
|
*/
|
||||||
static int getVideoStreamIndex(final String targetResolution, final MediaFormat targetFormat,
|
static int getVideoStreamIndex(@NonNull final String targetResolution,
|
||||||
final List<VideoStream> videoStreams) {
|
final MediaFormat targetFormat,
|
||||||
|
@NonNull final List<VideoStream> videoStreams) {
|
||||||
int fullMatchIndex = -1;
|
int fullMatchIndex = -1;
|
||||||
int fullMatchNoRefreshIndex = -1;
|
int fullMatchNoRefreshIndex = -1;
|
||||||
int resMatchOnlyIndex = -1;
|
int resMatchOnlyIndex = -1;
|
||||||
|
@ -428,7 +498,7 @@ public final class ListHelper {
|
||||||
* @param videoStreams the list of video streams to check
|
* @param videoStreams the list of video streams to check
|
||||||
* @return the index of the preferred video stream
|
* @return the index of the preferred video stream
|
||||||
*/
|
*/
|
||||||
private static int getDefaultResolutionWithDefaultFormat(final Context context,
|
private static int getDefaultResolutionWithDefaultFormat(@NonNull final Context context,
|
||||||
final String defaultResolution,
|
final String defaultResolution,
|
||||||
final List<VideoStream> videoStreams) {
|
final List<VideoStream> videoStreams) {
|
||||||
final MediaFormat defaultFormat = getDefaultFormat(context,
|
final MediaFormat defaultFormat = getDefaultFormat(context,
|
||||||
|
@ -437,7 +507,7 @@ public final class ListHelper {
|
||||||
context.getString(R.string.best_resolution_key), defaultFormat, videoStreams);
|
context.getString(R.string.best_resolution_key), defaultFormat, videoStreams);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MediaFormat getDefaultFormat(final Context context,
|
private static MediaFormat getDefaultFormat(@NonNull final Context context,
|
||||||
@StringRes final int defaultFormatKey,
|
@StringRes final int defaultFormatKey,
|
||||||
@StringRes final int defaultFormatValueKey) {
|
@StringRes final int defaultFormatValueKey) {
|
||||||
final SharedPreferences preferences
|
final SharedPreferences preferences
|
||||||
|
@ -457,8 +527,8 @@ public final class ListHelper {
|
||||||
return defaultMediaFormat;
|
return defaultMediaFormat;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MediaFormat getMediaFormatFromKey(final Context context,
|
private static MediaFormat getMediaFormatFromKey(@NonNull final Context context,
|
||||||
final String formatKey) {
|
@NonNull final String formatKey) {
|
||||||
MediaFormat format = null;
|
MediaFormat format = null;
|
||||||
if (formatKey.equals(context.getString(R.string.video_webm_key))) {
|
if (formatKey.equals(context.getString(R.string.video_webm_key))) {
|
||||||
format = MediaFormat.WEBM;
|
format = MediaFormat.WEBM;
|
||||||
|
@ -496,12 +566,20 @@ public final class ListHelper {
|
||||||
- formatRanking.indexOf(streamB.getFormat());
|
- formatRanking.indexOf(streamB.getFormat());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int compareVideoStreamResolution(final String r1, final String r2) {
|
private static int compareVideoStreamResolution(@NonNull final String r1,
|
||||||
final int res1 = Integer.parseInt(r1.replaceAll("0p\\d+$", "1")
|
@NonNull final String r2) {
|
||||||
.replaceAll("[^\\d.]", ""));
|
try {
|
||||||
final int res2 = Integer.parseInt(r2.replaceAll("0p\\d+$", "1")
|
final int res1 = Integer.parseInt(r1.replaceAll("0p\\d+$", "1")
|
||||||
.replaceAll("[^\\d.]", ""));
|
.replaceAll("[^\\d.]", ""));
|
||||||
return res1 - res2;
|
final int res2 = Integer.parseInt(r2.replaceAll("0p\\d+$", "1")
|
||||||
|
.replaceAll("[^\\d.]", ""));
|
||||||
|
return res1 - res2;
|
||||||
|
} catch (final NumberFormatException e) {
|
||||||
|
// Consider the first one greater because we don't know if the two streams are
|
||||||
|
// different or not (a NumberFormatException was thrown so we don't know the resolution
|
||||||
|
// of one stream or of all streams)
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compares the quality of two video streams.
|
// Compares the quality of two video streams.
|
||||||
|
@ -536,7 +614,7 @@ public final class ListHelper {
|
||||||
* @param context App context
|
* @param context App context
|
||||||
* @return maximum resolution allowed or null if there is no maximum
|
* @return maximum resolution allowed or null if there is no maximum
|
||||||
*/
|
*/
|
||||||
private static String getResolutionLimit(final Context context) {
|
private static String getResolutionLimit(@NonNull final Context context) {
|
||||||
String resolutionLimit = null;
|
String resolutionLimit = null;
|
||||||
if (isMeteredNetwork(context)) {
|
if (isMeteredNetwork(context)) {
|
||||||
final SharedPreferences preferences
|
final SharedPreferences preferences
|
||||||
|
@ -555,7 +633,7 @@ public final class ListHelper {
|
||||||
* @param context App context
|
* @param context App context
|
||||||
* @return {@code true} if connected to a metered network
|
* @return {@code true} if connected to a metered network
|
||||||
*/
|
*/
|
||||||
public static boolean isMeteredNetwork(final Context context) {
|
public static boolean isMeteredNetwork(@NonNull final Context context) {
|
||||||
final ConnectivityManager manager
|
final ConnectivityManager manager
|
||||||
= ContextCompat.getSystemService(context, ConnectivityManager.class);
|
= ContextCompat.getSystemService(context, ConnectivityManager.class);
|
||||||
if (manager == null || manager.getActiveNetworkInfo() == null) {
|
if (manager == null || manager.getActiveNetworkInfo() == null) {
|
||||||
|
|
|
@ -33,6 +33,7 @@ import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||||
|
import org.schabi.newpipe.extractor.stream.DeliveryMethod;
|
||||||
import org.schabi.newpipe.extractor.stream.Stream;
|
import org.schabi.newpipe.extractor.stream.Stream;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
|
@ -60,7 +61,9 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||||
import org.schabi.newpipe.settings.SettingsActivity;
|
import org.schabi.newpipe.settings.SettingsActivity;
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.util.ListHelper.getUrlAndNonTorrentStreams;
|
||||||
|
|
||||||
public final class NavigationHelper {
|
public final class NavigationHelper {
|
||||||
public static final String MAIN_FRAGMENT_TAG = "main_fragment_tag";
|
public static final String MAIN_FRAGMENT_TAG = "main_fragment_tag";
|
||||||
|
@ -217,30 +220,47 @@ public final class NavigationHelper {
|
||||||
|
|
||||||
public static void playOnExternalAudioPlayer(@NonNull final Context context,
|
public static void playOnExternalAudioPlayer(@NonNull final Context context,
|
||||||
@NonNull final StreamInfo info) {
|
@NonNull final StreamInfo info) {
|
||||||
final int index = ListHelper.getDefaultAudioFormat(context, info.getAudioStreams());
|
final List<AudioStream> audioStreams = info.getAudioStreams();
|
||||||
|
if (audioStreams == null || audioStreams.isEmpty()) {
|
||||||
if (index == -1) {
|
|
||||||
Toast.makeText(context, R.string.audio_streams_empty, Toast.LENGTH_SHORT).show();
|
Toast.makeText(context, R.string.audio_streams_empty, Toast.LENGTH_SHORT).show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final AudioStream audioStream = info.getAudioStreams().get(index);
|
final List<AudioStream> audioStreamsForExternalPlayers =
|
||||||
|
getUrlAndNonTorrentStreams(audioStreams);
|
||||||
|
if (audioStreamsForExternalPlayers.isEmpty()) {
|
||||||
|
Toast.makeText(context, R.string.no_audio_streams_available_for_external_players,
|
||||||
|
Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int index = ListHelper.getDefaultAudioFormat(context, audioStreamsForExternalPlayers);
|
||||||
|
final AudioStream audioStream = audioStreamsForExternalPlayers.get(index);
|
||||||
|
|
||||||
playOnExternalPlayer(context, info.getName(), info.getUploaderName(), audioStream);
|
playOnExternalPlayer(context, info.getName(), info.getUploaderName(), audioStream);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void playOnExternalVideoPlayer(@NonNull final Context context,
|
public static void playOnExternalVideoPlayer(final Context context,
|
||||||
@NonNull final StreamInfo info) {
|
@NonNull final StreamInfo info) {
|
||||||
final ArrayList<VideoStream> videoStreamsList = new ArrayList<>(
|
final List<VideoStream> videoStreams = info.getVideoStreams();
|
||||||
ListHelper.getSortedStreamVideosList(context, info.getVideoStreams(), null, false,
|
if (videoStreams == null || videoStreams.isEmpty()) {
|
||||||
false));
|
|
||||||
final int index = ListHelper.getDefaultResolutionIndex(context, videoStreamsList);
|
|
||||||
|
|
||||||
if (index == -1) {
|
|
||||||
Toast.makeText(context, R.string.video_streams_empty, Toast.LENGTH_SHORT).show();
|
Toast.makeText(context, R.string.video_streams_empty, Toast.LENGTH_SHORT).show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final VideoStream videoStream = videoStreamsList.get(index);
|
final List<VideoStream> videoStreamsForExternalPlayers =
|
||||||
|
ListHelper.getSortedStreamVideosList(context,
|
||||||
|
getUrlAndNonTorrentStreams(videoStreams), null, false, false);
|
||||||
|
if (videoStreamsForExternalPlayers.isEmpty()) {
|
||||||
|
Toast.makeText(context, R.string.no_video_streams_available_for_external_players,
|
||||||
|
Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int index = ListHelper.getDefaultResolutionIndex(context,
|
||||||
|
videoStreamsForExternalPlayers);
|
||||||
|
|
||||||
|
final VideoStream videoStream = videoStreamsForExternalPlayers.get(index);
|
||||||
playOnExternalPlayer(context, info.getName(), info.getUploaderName(), videoStream);
|
playOnExternalPlayer(context, info.getName(), info.getUploaderName(), videoStream);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -248,9 +268,48 @@ public final class NavigationHelper {
|
||||||
@Nullable final String name,
|
@Nullable final String name,
|
||||||
@Nullable final String artist,
|
@Nullable final String artist,
|
||||||
@NonNull final Stream stream) {
|
@NonNull final Stream stream) {
|
||||||
|
final DeliveryMethod deliveryMethod = stream.getDeliveryMethod();
|
||||||
|
final String mimeType;
|
||||||
|
|
||||||
|
if (!stream.isUrl() || deliveryMethod == DeliveryMethod.TORRENT) {
|
||||||
|
Toast.makeText(context, R.string.selected_stream_external_player_not_supported,
|
||||||
|
Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (deliveryMethod) {
|
||||||
|
case PROGRESSIVE_HTTP:
|
||||||
|
if (stream.getFormat() == null) {
|
||||||
|
if (stream instanceof AudioStream) {
|
||||||
|
mimeType = "audio/*";
|
||||||
|
} else if (stream instanceof VideoStream) {
|
||||||
|
mimeType = "video/*";
|
||||||
|
} else {
|
||||||
|
// This should never be reached, because subtitles are not opened in
|
||||||
|
// external players
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mimeType = stream.getFormat().getMimeType();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case HLS:
|
||||||
|
mimeType = "application/x-mpegURL";
|
||||||
|
break;
|
||||||
|
case DASH:
|
||||||
|
mimeType = "application/dash+xml";
|
||||||
|
break;
|
||||||
|
case SS:
|
||||||
|
mimeType = "application/vnd.ms-sstr+xml";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Torrent streams are not exposed to external players
|
||||||
|
mimeType = "";
|
||||||
|
}
|
||||||
|
|
||||||
final Intent intent = new Intent();
|
final Intent intent = new Intent();
|
||||||
intent.setAction(Intent.ACTION_VIEW);
|
intent.setAction(Intent.ACTION_VIEW);
|
||||||
intent.setDataAndType(Uri.parse(stream.getUrl()), stream.getFormat().getMimeType());
|
intent.setDataAndType(Uri.parse(stream.getContent()), mimeType);
|
||||||
intent.putExtra(Intent.EXTRA_TITLE, name);
|
intent.putExtra(Intent.EXTRA_TITLE, name);
|
||||||
intent.putExtra("title", name);
|
intent.putExtra("title", name);
|
||||||
intent.putExtra("artist", artist);
|
intent.putExtra("artist", artist);
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package org.schabi.newpipe.util;
|
package org.schabi.newpipe.util;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.MediaFormat;
|
import org.schabi.newpipe.extractor.MediaFormat;
|
||||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||||
|
@ -14,7 +15,8 @@ public class SecondaryStreamHelper<T extends Stream> {
|
||||||
private final int position;
|
private final int position;
|
||||||
private final StreamSizeWrapper<T> streams;
|
private final StreamSizeWrapper<T> streams;
|
||||||
|
|
||||||
public SecondaryStreamHelper(final StreamSizeWrapper<T> streams, final T selectedStream) {
|
public SecondaryStreamHelper(@NonNull final StreamSizeWrapper<T> streams,
|
||||||
|
final T selectedStream) {
|
||||||
this.streams = streams;
|
this.streams = streams;
|
||||||
this.position = streams.getStreamsList().indexOf(selectedStream);
|
this.position = streams.getStreamsList().indexOf(selectedStream);
|
||||||
if (this.position < 0) {
|
if (this.position < 0) {
|
||||||
|
@ -29,9 +31,15 @@ public class SecondaryStreamHelper<T extends Stream> {
|
||||||
* @param videoStream desired video ONLY stream
|
* @param videoStream desired video ONLY stream
|
||||||
* @return selected audio stream or null if a candidate was not found
|
* @return selected audio stream or null if a candidate was not found
|
||||||
*/
|
*/
|
||||||
|
@Nullable
|
||||||
public static AudioStream getAudioStreamFor(@NonNull final List<AudioStream> audioStreams,
|
public static AudioStream getAudioStreamFor(@NonNull final List<AudioStream> audioStreams,
|
||||||
@NonNull final VideoStream videoStream) {
|
@NonNull final VideoStream videoStream) {
|
||||||
switch (videoStream.getFormat()) {
|
final MediaFormat mediaFormat = videoStream.getFormat();
|
||||||
|
if (mediaFormat == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (mediaFormat) {
|
||||||
case WEBM:
|
case WEBM:
|
||||||
case MPEG_4:// ¿is mpeg-4 DASH?
|
case MPEG_4:// ¿is mpeg-4 DASH?
|
||||||
break;
|
break;
|
||||||
|
@ -39,7 +47,7 @@ public class SecondaryStreamHelper<T extends Stream> {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final boolean m4v = videoStream.getFormat() == MediaFormat.MPEG_4;
|
final boolean m4v = (mediaFormat == MediaFormat.MPEG_4);
|
||||||
|
|
||||||
for (final AudioStream audio : audioStreams) {
|
for (final AudioStream audio : audioStreams) {
|
||||||
if (audio.getFormat() == (m4v ? MediaFormat.M4A : MediaFormat.WEBMA)) {
|
if (audio.getFormat() == (m4v ? MediaFormat.M4A : MediaFormat.WEBMA)) {
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
package org.schabi.newpipe.util;
|
package org.schabi.newpipe.util;
|
||||||
|
|
||||||
import static org.schabi.newpipe.extractor.stream.StreamType.AUDIO_LIVE_STREAM;
|
|
||||||
import static org.schabi.newpipe.extractor.stream.StreamType.LIVE_STREAM;
|
|
||||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
@ -49,8 +47,8 @@ public final class SparseItemUtil {
|
||||||
public static void fetchItemInfoIfSparse(@NonNull final Context context,
|
public static void fetchItemInfoIfSparse(@NonNull final Context context,
|
||||||
@NonNull final StreamInfoItem item,
|
@NonNull final StreamInfoItem item,
|
||||||
@NonNull final Consumer<SinglePlayQueue> callback) {
|
@NonNull final Consumer<SinglePlayQueue> callback) {
|
||||||
if (((item.getStreamType() == LIVE_STREAM || item.getStreamType() == AUDIO_LIVE_STREAM)
|
if ((StreamTypeUtil.isLiveStream(item.getStreamType()) || item.getDuration() >= 0)
|
||||||
|| item.getDuration() >= 0) && !isNullOrEmpty(item.getUploaderUrl())) {
|
&& !isNullOrEmpty(item.getUploaderUrl())) {
|
||||||
// if the duration is >= 0 (provided that the item is not a livestream) and there is an
|
// if the duration is >= 0 (provided that the item is not a livestream) and there is an
|
||||||
// uploader url, probably all info is already there, so there is no need to fetch it
|
// uploader url, probably all info is already there, so there is no need to fetch it
|
||||||
callback.accept(new SinglePlayQueue(item));
|
callback.accept(new SinglePlayQueue(item));
|
||||||
|
|
|
@ -10,6 +10,8 @@ import android.widget.ImageView;
|
||||||
import android.widget.Spinner;
|
import android.widget.Spinner;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import org.schabi.newpipe.DownloaderImpl;
|
import org.schabi.newpipe.DownloaderImpl;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.extractor.MediaFormat;
|
import org.schabi.newpipe.extractor.MediaFormat;
|
||||||
|
@ -87,7 +89,8 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View getDropDownView(final int position, final View convertView,
|
public View getDropDownView(final int position,
|
||||||
|
final View convertView,
|
||||||
final ViewGroup parent) {
|
final ViewGroup parent) {
|
||||||
return getCustomView(position, convertView, parent, true);
|
return getCustomView(position, convertView, parent, true);
|
||||||
}
|
}
|
||||||
|
@ -98,7 +101,10 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
|
||||||
convertView, parent, false);
|
convertView, parent, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private View getCustomView(final int position, final View view, final ViewGroup parent,
|
@NonNull
|
||||||
|
private View getCustomView(final int position,
|
||||||
|
final View view,
|
||||||
|
final ViewGroup parent,
|
||||||
final boolean isDropdownItem) {
|
final boolean isDropdownItem) {
|
||||||
View convertView = view;
|
View convertView = view;
|
||||||
if (convertView == null) {
|
if (convertView == null) {
|
||||||
|
@ -112,6 +118,7 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
|
||||||
final TextView sizeView = convertView.findViewById(R.id.stream_size);
|
final TextView sizeView = convertView.findViewById(R.id.stream_size);
|
||||||
|
|
||||||
final T stream = getItem(position);
|
final T stream = getItem(position);
|
||||||
|
final MediaFormat mediaFormat = stream.getFormat();
|
||||||
|
|
||||||
int woSoundIconVisibility = View.GONE;
|
int woSoundIconVisibility = View.GONE;
|
||||||
String qualityString;
|
String qualityString;
|
||||||
|
@ -135,24 +142,32 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
|
||||||
}
|
}
|
||||||
} else if (stream instanceof AudioStream) {
|
} else if (stream instanceof AudioStream) {
|
||||||
final AudioStream audioStream = ((AudioStream) stream);
|
final AudioStream audioStream = ((AudioStream) stream);
|
||||||
qualityString = audioStream.getAverageBitrate() > 0
|
if (audioStream.getAverageBitrate() > 0) {
|
||||||
? audioStream.getAverageBitrate() + "kbps"
|
qualityString = audioStream.getAverageBitrate() + "kbps";
|
||||||
: audioStream.getFormat().getName();
|
} else if (mediaFormat != null) {
|
||||||
|
qualityString = mediaFormat.getName();
|
||||||
|
} else {
|
||||||
|
qualityString = context.getString(R.string.unknown_quality);
|
||||||
|
}
|
||||||
} else if (stream instanceof SubtitlesStream) {
|
} else if (stream instanceof SubtitlesStream) {
|
||||||
qualityString = ((SubtitlesStream) stream).getDisplayLanguageName();
|
qualityString = ((SubtitlesStream) stream).getDisplayLanguageName();
|
||||||
if (((SubtitlesStream) stream).isAutoGenerated()) {
|
if (((SubtitlesStream) stream).isAutoGenerated()) {
|
||||||
qualityString += " (" + context.getString(R.string.caption_auto_generated) + ")";
|
qualityString += " (" + context.getString(R.string.caption_auto_generated) + ")";
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
qualityString = stream.getFormat().getSuffix();
|
if (mediaFormat == null) {
|
||||||
|
qualityString = context.getString(R.string.unknown_quality);
|
||||||
|
} else {
|
||||||
|
qualityString = mediaFormat.getSuffix();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (streamsWrapper.getSizeInBytes(position) > 0) {
|
if (streamsWrapper.getSizeInBytes(position) > 0) {
|
||||||
final SecondaryStreamHelper<U> secondary = secondaryStreams == null ? null
|
final SecondaryStreamHelper<U> secondary = secondaryStreams == null ? null
|
||||||
: secondaryStreams.get(position);
|
: secondaryStreams.get(position);
|
||||||
if (secondary != null) {
|
if (secondary != null) {
|
||||||
final long size
|
final long size = secondary.getSizeInBytes()
|
||||||
= secondary.getSizeInBytes() + streamsWrapper.getSizeInBytes(position);
|
+ streamsWrapper.getSizeInBytes(position);
|
||||||
sizeView.setText(Utility.formatBytes(size));
|
sizeView.setText(Utility.formatBytes(size));
|
||||||
} else {
|
} else {
|
||||||
sizeView.setText(streamsWrapper.getFormattedSize(position));
|
sizeView.setText(streamsWrapper.getFormattedSize(position));
|
||||||
|
@ -164,11 +179,15 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
|
||||||
|
|
||||||
if (stream instanceof SubtitlesStream) {
|
if (stream instanceof SubtitlesStream) {
|
||||||
formatNameView.setText(((SubtitlesStream) stream).getLanguageTag());
|
formatNameView.setText(((SubtitlesStream) stream).getLanguageTag());
|
||||||
} else if (stream.getFormat() == MediaFormat.WEBMA_OPUS) {
|
|
||||||
// noinspection AndroidLintSetTextI18n
|
|
||||||
formatNameView.setText("opus");
|
|
||||||
} else {
|
} else {
|
||||||
formatNameView.setText(stream.getFormat().getName());
|
if (mediaFormat == null) {
|
||||||
|
formatNameView.setText(context.getString(R.string.unknown_format));
|
||||||
|
} else if (mediaFormat == MediaFormat.WEBMA_OPUS) {
|
||||||
|
// noinspection AndroidLintSetTextI18n
|
||||||
|
formatNameView.setText("opus");
|
||||||
|
} else {
|
||||||
|
formatNameView.setText(mediaFormat.getName());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
qualityView.setText(qualityString);
|
qualityView.setText(qualityString);
|
||||||
|
@ -233,6 +252,7 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
|
||||||
* @param streamsWrapper the wrapper
|
* @param streamsWrapper the wrapper
|
||||||
* @return a {@link Single} that returns a boolean indicating if any elements were changed
|
* @return a {@link Single} that returns a boolean indicating if any elements were changed
|
||||||
*/
|
*/
|
||||||
|
@NonNull
|
||||||
public static <X extends Stream> Single<Boolean> fetchSizeForWrapper(
|
public static <X extends Stream> Single<Boolean> fetchSizeForWrapper(
|
||||||
final StreamSizeWrapper<X> streamsWrapper) {
|
final StreamSizeWrapper<X> streamsWrapper) {
|
||||||
final Callable<Boolean> fetchAndSet = () -> {
|
final Callable<Boolean> fetchAndSet = () -> {
|
||||||
|
@ -243,7 +263,7 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
|
||||||
}
|
}
|
||||||
|
|
||||||
final long contentLength = DownloaderImpl.getInstance().getContentLength(
|
final long contentLength = DownloaderImpl.getInstance().getContentLength(
|
||||||
stream.getUrl());
|
stream.getContent());
|
||||||
streamsWrapper.setSize(stream, contentLength);
|
streamsWrapper.setSize(stream, contentLength);
|
||||||
hasChanged = true;
|
hasChanged = true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ package org.schabi.newpipe.util;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility class for {@link org.schabi.newpipe.extractor.stream.StreamType}.
|
* Utility class for {@link StreamType}.
|
||||||
*/
|
*/
|
||||||
public final class StreamTypeUtil {
|
public final class StreamTypeUtil {
|
||||||
private StreamTypeUtil() {
|
private StreamTypeUtil() {
|
||||||
|
@ -11,11 +11,37 @@ public final class StreamTypeUtil {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the streamType is a livestream.
|
* Check if the {@link StreamType} of a stream is a livestream.
|
||||||
*
|
*
|
||||||
* @param streamType
|
* @param streamType the stream type of the stream
|
||||||
* @return <code>true</code> when the streamType is a
|
* @return whether the stream type is {@link StreamType#AUDIO_STREAM},
|
||||||
* {@link StreamType#LIVE_STREAM} or {@link StreamType#AUDIO_LIVE_STREAM}
|
* {@link StreamType#AUDIO_LIVE_STREAM} or {@link StreamType#POST_LIVE_AUDIO_STREAM}
|
||||||
|
*/
|
||||||
|
public static boolean isAudio(final StreamType streamType) {
|
||||||
|
return streamType == StreamType.AUDIO_STREAM
|
||||||
|
|| streamType == StreamType.AUDIO_LIVE_STREAM
|
||||||
|
|| streamType == StreamType.POST_LIVE_AUDIO_STREAM;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the {@link StreamType} of a stream is a livestream.
|
||||||
|
*
|
||||||
|
* @param streamType the stream type of the stream
|
||||||
|
* @return whether the stream type is {@link StreamType#VIDEO_STREAM},
|
||||||
|
* {@link StreamType#LIVE_STREAM} or {@link StreamType#POST_LIVE_STREAM}
|
||||||
|
*/
|
||||||
|
public static boolean isVideo(final StreamType streamType) {
|
||||||
|
return streamType == StreamType.VIDEO_STREAM
|
||||||
|
|| streamType == StreamType.LIVE_STREAM
|
||||||
|
|| streamType == StreamType.POST_LIVE_STREAM;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the {@link StreamType} of a stream is a livestream.
|
||||||
|
*
|
||||||
|
* @param streamType the stream type of the stream
|
||||||
|
* @return whether the stream type is {@link StreamType#LIVE_STREAM} or
|
||||||
|
* {@link StreamType#AUDIO_LIVE_STREAM}
|
||||||
*/
|
*/
|
||||||
public static boolean isLiveStream(final StreamType streamType) {
|
public static boolean isLiveStream(final StreamType streamType) {
|
||||||
return streamType == StreamType.LIVE_STREAM
|
return streamType == StreamType.LIVE_STREAM
|
||||||
|
|
|
@ -6,6 +6,7 @@ import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||||
|
import org.schabi.newpipe.extractor.stream.DeliveryMethod;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamExtractor;
|
import org.schabi.newpipe.extractor.stream.StreamExtractor;
|
||||||
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
||||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||||
|
@ -131,31 +132,38 @@ public class DownloadMissionRecover extends Thread {
|
||||||
|
|
||||||
switch (mRecovery.getKind()) {
|
switch (mRecovery.getKind()) {
|
||||||
case 'a':
|
case 'a':
|
||||||
for (AudioStream audio : mExtractor.getAudioStreams()) {
|
for (final AudioStream audio : mExtractor.getAudioStreams()) {
|
||||||
if (audio.getAverageBitrate() == mRecovery.getDesiredBitrate() && audio.getFormat() == mRecovery.getFormat()) {
|
if (audio.getAverageBitrate() == mRecovery.getDesiredBitrate()
|
||||||
url = audio.getUrl();
|
&& audio.getFormat() == mRecovery.getFormat()
|
||||||
|
&& audio.getDeliveryMethod() == DeliveryMethod.PROGRESSIVE_HTTP) {
|
||||||
|
url = audio.getContent();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'v':
|
case 'v':
|
||||||
List<VideoStream> videoStreams;
|
final List<VideoStream> videoStreams;
|
||||||
if (mRecovery.isDesired2())
|
if (mRecovery.isDesired2())
|
||||||
videoStreams = mExtractor.getVideoOnlyStreams();
|
videoStreams = mExtractor.getVideoOnlyStreams();
|
||||||
else
|
else
|
||||||
videoStreams = mExtractor.getVideoStreams();
|
videoStreams = mExtractor.getVideoStreams();
|
||||||
for (VideoStream video : videoStreams) {
|
for (final VideoStream video : videoStreams) {
|
||||||
if (video.resolution.equals(mRecovery.getDesired()) && video.getFormat() == mRecovery.getFormat()) {
|
if (video.getResolution().equals(mRecovery.getDesired())
|
||||||
url = video.getUrl();
|
&& video.getFormat() == mRecovery.getFormat()
|
||||||
|
&& video.getDeliveryMethod() == DeliveryMethod.PROGRESSIVE_HTTP) {
|
||||||
|
url = video.getContent();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 's':
|
case 's':
|
||||||
for (SubtitlesStream subtitles : mExtractor.getSubtitles(mRecovery.getFormat())) {
|
for (final SubtitlesStream subtitles : mExtractor.getSubtitles(mRecovery
|
||||||
|
.getFormat())) {
|
||||||
String tag = subtitles.getLanguageTag();
|
String tag = subtitles.getLanguageTag();
|
||||||
if (tag.equals(mRecovery.getDesired()) && subtitles.isAutoGenerated() == mRecovery.isDesired2()) {
|
if (tag.equals(mRecovery.getDesired())
|
||||||
url = subtitles.getUrl();
|
&& subtitles.isAutoGenerated() == mRecovery.isDesired2()
|
||||||
|
&& subtitles.getDeliveryMethod() == DeliveryMethod.PROGRESSIVE_HTTP) {
|
||||||
|
url = subtitles.getContent();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,23 +11,23 @@ import java.io.Serializable
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class MissionRecoveryInfo(
|
class MissionRecoveryInfo(
|
||||||
var format: MediaFormat,
|
var format: MediaFormat?,
|
||||||
var desired: String? = null,
|
var desired: String? = null,
|
||||||
var isDesired2: Boolean = false,
|
var isDesired2: Boolean = false,
|
||||||
var desiredBitrate: Int = 0,
|
var desiredBitrate: Int = 0,
|
||||||
var kind: Char = Char.MIN_VALUE,
|
var kind: Char = Char.MIN_VALUE,
|
||||||
var validateCondition: String? = null
|
var validateCondition: String? = null
|
||||||
) : Serializable, Parcelable {
|
) : Serializable, Parcelable {
|
||||||
constructor(stream: Stream) : this(format = stream.getFormat()!!) {
|
constructor(stream: Stream) : this(format = stream.format) {
|
||||||
when (stream) {
|
when (stream) {
|
||||||
is AudioStream -> {
|
is AudioStream -> {
|
||||||
desiredBitrate = stream.averageBitrate
|
desiredBitrate = stream.getAverageBitrate()
|
||||||
isDesired2 = false
|
isDesired2 = false
|
||||||
kind = 'a'
|
kind = 'a'
|
||||||
}
|
}
|
||||||
is VideoStream -> {
|
is VideoStream -> {
|
||||||
desired = stream.resolution
|
desired = stream.getResolution()
|
||||||
isDesired2 = stream.isVideoOnly
|
isDesired2 = stream.isVideoOnly()
|
||||||
kind = 'v'
|
kind = 'v'
|
||||||
}
|
}
|
||||||
is SubtitlesStream -> {
|
is SubtitlesStream -> {
|
||||||
|
@ -62,7 +62,7 @@ class MissionRecoveryInfo(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
str.append(" format=")
|
str.append(" format=")
|
||||||
.append(format.getName())
|
.append(format?.getName())
|
||||||
.append(' ')
|
.append(' ')
|
||||||
.append(info)
|
.append(info)
|
||||||
.append('}')
|
.append('}')
|
||||||
|
|
|
@ -82,13 +82,14 @@
|
||||||
android:text="@string/msg_threads" />
|
android:text="@string/msg_threads" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
android:id="@+id/threads_layout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_below="@+id/threads_text_view"
|
android:layout_below="@+id/threads_text_view"
|
||||||
android:layout_marginLeft="24dp"
|
android:layout_marginLeft="24dp"
|
||||||
android:layout_marginRight="24dp"
|
android:layout_marginRight="24dp"
|
||||||
android:orientation="horizontal"
|
android:layout_marginBottom="12dp"
|
||||||
android:paddingBottom="12dp">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
<org.schabi.newpipe.views.NewPipeTextView
|
<org.schabi.newpipe.views.NewPipeTextView
|
||||||
android:id="@+id/threads_count"
|
android:id="@+id/threads_count"
|
||||||
|
@ -106,4 +107,16 @@
|
||||||
android:max="31"
|
android:max="31"
|
||||||
android:progress="3" />
|
android:progress="3" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<org.schabi.newpipe.views.NewPipeTextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_below="@+id/threads_layout"
|
||||||
|
android:layout_marginLeft="24dp"
|
||||||
|
android:layout_marginRight="24dp"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/streams_not_yet_supported_removed"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
|
@ -740,4 +740,11 @@
|
||||||
<string name="you_successfully_subscribed">You now subscribed to this channel</string>
|
<string name="you_successfully_subscribed">You now subscribed to this channel</string>
|
||||||
<string name="enumeration_comma">,</string>
|
<string name="enumeration_comma">,</string>
|
||||||
<string name="toggle_all">Toggle all</string>
|
<string name="toggle_all">Toggle all</string>
|
||||||
|
<string name="streams_not_yet_supported_removed">Streams which are not yet supported by the downloader are not shown</string>
|
||||||
|
<string name="selected_stream_external_player_not_supported">The selected stream is not supported by external players</string>
|
||||||
|
<string name="no_audio_streams_available_for_external_players">No audio streams are available for external players</string>
|
||||||
|
<string name="no_video_streams_available_for_external_players">No video streams are available for external players</string>
|
||||||
|
<string name="select_quality_external_players">Select quality for external players</string>
|
||||||
|
<string name="unknown_format">Unknown format</string>
|
||||||
|
<string name="unknown_quality">Unknown quality</string>
|
||||||
</resources>
|
</resources>
|
|
@ -13,38 +13,41 @@ import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
public class ListHelperTest {
|
public class ListHelperTest {
|
||||||
private static final String BEST_RESOLUTION_KEY = "best_resolution";
|
private static final String BEST_RESOLUTION_KEY = "best_resolution";
|
||||||
private static final List<AudioStream> AUDIO_STREAMS_TEST_LIST = Arrays.asList(
|
private static final List<AudioStream> AUDIO_STREAMS_TEST_LIST = Arrays.asList(
|
||||||
new AudioStream("", MediaFormat.M4A, /**/ 128),
|
generateAudioStream("m4a-128-1", MediaFormat.M4A, 128),
|
||||||
new AudioStream("", MediaFormat.WEBMA, /**/ 192),
|
generateAudioStream("webma-192", MediaFormat.WEBMA, 192),
|
||||||
new AudioStream("", MediaFormat.MP3, /**/ 64),
|
generateAudioStream("mp3-64", MediaFormat.MP3, 64),
|
||||||
new AudioStream("", MediaFormat.WEBMA, /**/ 192),
|
generateAudioStream("webma-192", MediaFormat.WEBMA, 192),
|
||||||
new AudioStream("", MediaFormat.M4A, /**/ 128),
|
generateAudioStream("m4a-128-2", MediaFormat.M4A, 128),
|
||||||
new AudioStream("", MediaFormat.MP3, /**/ 128),
|
generateAudioStream("mp3-128", MediaFormat.MP3, 128),
|
||||||
new AudioStream("", MediaFormat.WEBMA, /**/ 64),
|
generateAudioStream("webma-64", MediaFormat.WEBMA, 64),
|
||||||
new AudioStream("", MediaFormat.M4A, /**/ 320),
|
generateAudioStream("m4a-320", MediaFormat.M4A, 320),
|
||||||
new AudioStream("", MediaFormat.MP3, /**/ 192),
|
generateAudioStream("mp3-192", MediaFormat.MP3, 192),
|
||||||
new AudioStream("", MediaFormat.WEBMA, /**/ 320));
|
generateAudioStream("webma-320", MediaFormat.WEBMA, 320));
|
||||||
|
|
||||||
private static final List<VideoStream> VIDEO_STREAMS_TEST_LIST = Arrays.asList(
|
private static final List<VideoStream> VIDEO_STREAMS_TEST_LIST = Arrays.asList(
|
||||||
new VideoStream("", MediaFormat.MPEG_4, /**/ "720p"),
|
generateVideoStream("mpeg_4-720", MediaFormat.MPEG_4, "720p", false),
|
||||||
new VideoStream("", MediaFormat.v3GPP, /**/ "240p"),
|
generateVideoStream("v3gpp-240", MediaFormat.v3GPP, "240p", false),
|
||||||
new VideoStream("", MediaFormat.WEBM, /**/ "480p"),
|
generateVideoStream("webm-480", MediaFormat.WEBM, "480p", false),
|
||||||
new VideoStream("", MediaFormat.v3GPP, /**/ "144p"),
|
generateVideoStream("v3gpp-144", MediaFormat.v3GPP, "144p", false),
|
||||||
new VideoStream("", MediaFormat.MPEG_4, /**/ "360p"),
|
generateVideoStream("mpeg_4-360", MediaFormat.MPEG_4, "360p", false),
|
||||||
new VideoStream("", MediaFormat.WEBM, /**/ "360p"));
|
generateVideoStream("webm-360", MediaFormat.WEBM, "360p", false));
|
||||||
|
|
||||||
private static final List<VideoStream> VIDEO_ONLY_STREAMS_TEST_LIST = Arrays.asList(
|
private static final List<VideoStream> VIDEO_ONLY_STREAMS_TEST_LIST = Arrays.asList(
|
||||||
new VideoStream("", MediaFormat.MPEG_4, /**/ "720p", true),
|
generateVideoStream("mpeg_4-720-1", MediaFormat.MPEG_4, "720p", true),
|
||||||
new VideoStream("", MediaFormat.MPEG_4, /**/ "720p", true),
|
generateVideoStream("mpeg_4-720-2", MediaFormat.MPEG_4, "720p", true),
|
||||||
new VideoStream("", MediaFormat.MPEG_4, /**/ "2160p", true),
|
generateVideoStream("mpeg_4-2160", MediaFormat.MPEG_4, "2160p", true),
|
||||||
new VideoStream("", MediaFormat.MPEG_4, /**/ "1440p60", true),
|
generateVideoStream("mpeg_4-1440_60", MediaFormat.MPEG_4, "1440p60", true),
|
||||||
new VideoStream("", MediaFormat.WEBM, /**/ "720p60", true),
|
generateVideoStream("webm-720_60", MediaFormat.WEBM, "720p60", true),
|
||||||
new VideoStream("", MediaFormat.MPEG_4, /**/ "2160p60", true),
|
generateVideoStream("mpeg_4-2160_60", MediaFormat.MPEG_4, "2160p60", true),
|
||||||
new VideoStream("", MediaFormat.MPEG_4, /**/ "720p60", true),
|
generateVideoStream("mpeg_4-720_60", MediaFormat.MPEG_4, "720p60", true),
|
||||||
new VideoStream("", MediaFormat.MPEG_4, /**/ "1080p", true),
|
generateVideoStream("mpeg_4-1080", MediaFormat.MPEG_4, "1080p", true),
|
||||||
new VideoStream("", MediaFormat.MPEG_4, /**/ "1080p60", true));
|
generateVideoStream("mpeg_4-1080_60", MediaFormat.MPEG_4, "1080p60", true));
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getSortedStreamVideosListTest() {
|
public void getSortedStreamVideosListTest() {
|
||||||
|
@ -56,7 +59,8 @@ public class ListHelperTest {
|
||||||
|
|
||||||
assertEquals(expected.size(), result.size());
|
assertEquals(expected.size(), result.size());
|
||||||
for (int i = 0; i < result.size(); i++) {
|
for (int i = 0; i < result.size(); i++) {
|
||||||
assertEquals(expected.get(i), result.get(i).resolution);
|
assertEquals(result.get(i).getResolution(), expected.get(i));
|
||||||
|
assertEquals(expected.get(i), result.get(i).getResolution());
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////
|
////////////////////
|
||||||
|
@ -69,7 +73,7 @@ public class ListHelperTest {
|
||||||
"720p", "480p", "360p", "240p", "144p");
|
"720p", "480p", "360p", "240p", "144p");
|
||||||
assertEquals(expected.size(), result.size());
|
assertEquals(expected.size(), result.size());
|
||||||
for (int i = 0; i < result.size(); i++) {
|
for (int i = 0; i < result.size(); i++) {
|
||||||
assertEquals(expected.get(i), result.get(i).resolution);
|
assertEquals(expected.get(i), result.get(i).getResolution());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,8 +87,8 @@ public class ListHelperTest {
|
||||||
|
|
||||||
assertEquals(expected.size(), result.size());
|
assertEquals(expected.size(), result.size());
|
||||||
for (int i = 0; i < result.size(); i++) {
|
for (int i = 0; i < result.size(); i++) {
|
||||||
assertEquals(expected.get(i), result.get(i).resolution);
|
assertEquals(expected.get(i), result.get(i).getResolution());
|
||||||
assertTrue(result.get(i).isVideoOnly);
|
assertTrue(result.get(i).isVideoOnly());
|
||||||
}
|
}
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////
|
||||||
|
@ -96,8 +100,8 @@ public class ListHelperTest {
|
||||||
expected = Arrays.asList("720p", "480p", "360p", "240p", "144p");
|
expected = Arrays.asList("720p", "480p", "360p", "240p", "144p");
|
||||||
assertEquals(expected.size(), result.size());
|
assertEquals(expected.size(), result.size());
|
||||||
for (int i = 0; i < result.size(); i++) {
|
for (int i = 0; i < result.size(); i++) {
|
||||||
assertEquals(expected.get(i), result.get(i).resolution);
|
assertEquals(expected.get(i), result.get(i).getResolution());
|
||||||
assertFalse(result.get(i).isVideoOnly);
|
assertFalse(result.get(i).isVideoOnly());
|
||||||
}
|
}
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////
|
||||||
|
@ -113,10 +117,9 @@ public class ListHelperTest {
|
||||||
|
|
||||||
assertEquals(expected.size(), result.size());
|
assertEquals(expected.size(), result.size());
|
||||||
for (int i = 0; i < result.size(); i++) {
|
for (int i = 0; i < result.size(); i++) {
|
||||||
assertEquals(expected.get(i), result.get(i).resolution);
|
assertEquals(expected.get(i), result.get(i).getResolution());
|
||||||
assertEquals(
|
assertEquals(expectedVideoOnly.contains(result.get(i).getResolution()),
|
||||||
expectedVideoOnly.contains(result.get(i).resolution),
|
result.get(i).isVideoOnly());
|
||||||
result.get(i).isVideoOnly);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,66 +135,66 @@ public class ListHelperTest {
|
||||||
"1080p60", "1080p", "720p60", "720p", "480p", "360p", "240p", "144p");
|
"1080p60", "1080p", "720p60", "720p", "480p", "360p", "240p", "144p");
|
||||||
assertEquals(expected.size(), result.size());
|
assertEquals(expected.size(), result.size());
|
||||||
for (int i = 0; i < result.size(); i++) {
|
for (int i = 0; i < result.size(); i++) {
|
||||||
assertEquals(expected.get(i), result.get(i).resolution);
|
assertEquals(expected.get(i), result.get(i).getResolution());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getDefaultResolutionTest() {
|
public void getDefaultResolutionTest() {
|
||||||
final List<VideoStream> testList = Arrays.asList(
|
final List<VideoStream> testList = Arrays.asList(
|
||||||
new VideoStream("", MediaFormat.MPEG_4, /**/ "720p"),
|
generateVideoStream("mpeg_4-720", MediaFormat.MPEG_4, "720p", false),
|
||||||
new VideoStream("", MediaFormat.v3GPP, /**/ "240p"),
|
generateVideoStream("v3gpp-240", MediaFormat.v3GPP, "240p", false),
|
||||||
new VideoStream("", MediaFormat.WEBM, /**/ "480p"),
|
generateVideoStream("webm-480", MediaFormat.WEBM, "480p", false),
|
||||||
new VideoStream("", MediaFormat.WEBM, /**/ "240p"),
|
generateVideoStream("webm-240", MediaFormat.WEBM, "240p", false),
|
||||||
new VideoStream("", MediaFormat.MPEG_4, /**/ "240p"),
|
generateVideoStream("mpeg_4-240", MediaFormat.MPEG_4, "240p", false),
|
||||||
new VideoStream("", MediaFormat.WEBM, /**/ "144p"),
|
generateVideoStream("webm-144", MediaFormat.WEBM, "144p", false),
|
||||||
new VideoStream("", MediaFormat.MPEG_4, /**/ "360p"),
|
generateVideoStream("mpeg_4-360", MediaFormat.MPEG_4, "360p", false),
|
||||||
new VideoStream("", MediaFormat.WEBM, /**/ "360p"));
|
generateVideoStream("webm-360", MediaFormat.WEBM, "360p", false));
|
||||||
VideoStream result = testList.get(ListHelper.getDefaultResolutionIndex(
|
VideoStream result = testList.get(ListHelper.getDefaultResolutionIndex(
|
||||||
"720p", BEST_RESOLUTION_KEY, MediaFormat.MPEG_4, testList));
|
"720p", BEST_RESOLUTION_KEY, MediaFormat.MPEG_4, testList));
|
||||||
assertEquals("720p", result.resolution);
|
assertEquals("720p", result.getResolution());
|
||||||
assertEquals(MediaFormat.MPEG_4, result.getFormat());
|
assertEquals(MediaFormat.MPEG_4, result.getFormat());
|
||||||
|
|
||||||
// Have resolution and the format
|
// Have resolution and the format
|
||||||
result = testList.get(ListHelper.getDefaultResolutionIndex(
|
result = testList.get(ListHelper.getDefaultResolutionIndex(
|
||||||
"480p", BEST_RESOLUTION_KEY, MediaFormat.WEBM, testList));
|
"480p", BEST_RESOLUTION_KEY, MediaFormat.WEBM, testList));
|
||||||
assertEquals("480p", result.resolution);
|
assertEquals("480p", result.getResolution());
|
||||||
assertEquals(MediaFormat.WEBM, result.getFormat());
|
assertEquals(MediaFormat.WEBM, result.getFormat());
|
||||||
|
|
||||||
// Have resolution but not the format
|
// Have resolution but not the format
|
||||||
result = testList.get(ListHelper.getDefaultResolutionIndex(
|
result = testList.get(ListHelper.getDefaultResolutionIndex(
|
||||||
"480p", BEST_RESOLUTION_KEY, MediaFormat.MPEG_4, testList));
|
"480p", BEST_RESOLUTION_KEY, MediaFormat.MPEG_4, testList));
|
||||||
assertEquals("480p", result.resolution);
|
assertEquals("480p", result.getResolution());
|
||||||
assertEquals(MediaFormat.WEBM, result.getFormat());
|
assertEquals(MediaFormat.WEBM, result.getFormat());
|
||||||
|
|
||||||
// Have resolution and the format
|
// Have resolution and the format
|
||||||
result = testList.get(ListHelper.getDefaultResolutionIndex(
|
result = testList.get(ListHelper.getDefaultResolutionIndex(
|
||||||
"240p", BEST_RESOLUTION_KEY, MediaFormat.WEBM, testList));
|
"240p", BEST_RESOLUTION_KEY, MediaFormat.WEBM, testList));
|
||||||
assertEquals("240p", result.resolution);
|
assertEquals("240p", result.getResolution());
|
||||||
assertEquals(MediaFormat.WEBM, result.getFormat());
|
assertEquals(MediaFormat.WEBM, result.getFormat());
|
||||||
|
|
||||||
// The best resolution
|
// The best resolution
|
||||||
result = testList.get(ListHelper.getDefaultResolutionIndex(
|
result = testList.get(ListHelper.getDefaultResolutionIndex(
|
||||||
BEST_RESOLUTION_KEY, BEST_RESOLUTION_KEY, MediaFormat.WEBM, testList));
|
BEST_RESOLUTION_KEY, BEST_RESOLUTION_KEY, MediaFormat.WEBM, testList));
|
||||||
assertEquals("720p", result.resolution);
|
assertEquals("720p", result.getResolution());
|
||||||
assertEquals(MediaFormat.MPEG_4, result.getFormat());
|
assertEquals(MediaFormat.MPEG_4, result.getFormat());
|
||||||
|
|
||||||
// Doesn't have the 60fps variant and format
|
// Doesn't have the 60fps variant and format
|
||||||
result = testList.get(ListHelper.getDefaultResolutionIndex(
|
result = testList.get(ListHelper.getDefaultResolutionIndex(
|
||||||
"720p60", BEST_RESOLUTION_KEY, MediaFormat.WEBM, testList));
|
"720p60", BEST_RESOLUTION_KEY, MediaFormat.WEBM, testList));
|
||||||
assertEquals("720p", result.resolution);
|
assertEquals("720p", result.getResolution());
|
||||||
assertEquals(MediaFormat.MPEG_4, result.getFormat());
|
assertEquals(MediaFormat.MPEG_4, result.getFormat());
|
||||||
|
|
||||||
// Doesn't have the 60fps variant
|
// Doesn't have the 60fps variant
|
||||||
result = testList.get(ListHelper.getDefaultResolutionIndex(
|
result = testList.get(ListHelper.getDefaultResolutionIndex(
|
||||||
"480p60", BEST_RESOLUTION_KEY, MediaFormat.WEBM, testList));
|
"480p60", BEST_RESOLUTION_KEY, MediaFormat.WEBM, testList));
|
||||||
assertEquals("480p", result.resolution);
|
assertEquals("480p", result.getResolution());
|
||||||
assertEquals(MediaFormat.WEBM, result.getFormat());
|
assertEquals(MediaFormat.WEBM, result.getFormat());
|
||||||
|
|
||||||
// Doesn't have the resolution, will return the best one
|
// Doesn't have the resolution, will return the best one
|
||||||
result = testList.get(ListHelper.getDefaultResolutionIndex(
|
result = testList.get(ListHelper.getDefaultResolutionIndex(
|
||||||
"2160p60", BEST_RESOLUTION_KEY, MediaFormat.WEBM, testList));
|
"2160p60", BEST_RESOLUTION_KEY, MediaFormat.WEBM, testList));
|
||||||
assertEquals("720p", result.resolution);
|
assertEquals("720p", result.getResolution());
|
||||||
assertEquals(MediaFormat.MPEG_4, result.getFormat());
|
assertEquals(MediaFormat.MPEG_4, result.getFormat());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -221,8 +224,8 @@ public class ListHelperTest {
|
||||||
////////////////////////////////////////
|
////////////////////////////////////////
|
||||||
|
|
||||||
List<AudioStream> testList = Arrays.asList(
|
List<AudioStream> testList = Arrays.asList(
|
||||||
new AudioStream("", MediaFormat.M4A, /**/ 128),
|
generateAudioStream("m4a-128", MediaFormat.M4A, 128),
|
||||||
new AudioStream("", MediaFormat.WEBMA, /**/ 192));
|
generateAudioStream("webma-192", MediaFormat.WEBMA, 192));
|
||||||
// List doesn't contains this format
|
// List doesn't contains this format
|
||||||
// It should fallback to the highest bitrate audio no matter what format it is
|
// It should fallback to the highest bitrate audio no matter what format it is
|
||||||
AudioStream stream = testList.get(ListHelper.getHighestQualityAudioIndex(
|
AudioStream stream = testList.get(ListHelper.getHighestQualityAudioIndex(
|
||||||
|
@ -235,13 +238,13 @@ public class ListHelperTest {
|
||||||
//////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////
|
||||||
|
|
||||||
testList = new ArrayList<>(Arrays.asList(
|
testList = new ArrayList<>(Arrays.asList(
|
||||||
new AudioStream("", MediaFormat.WEBMA, /**/ 192),
|
generateAudioStream("webma-192-1", MediaFormat.WEBMA, 192),
|
||||||
new AudioStream("", MediaFormat.M4A, /**/ 192),
|
generateAudioStream("m4a-192-1", MediaFormat.M4A, 192),
|
||||||
new AudioStream("", MediaFormat.WEBMA, /**/ 192),
|
generateAudioStream("webma-192-2", MediaFormat.WEBMA, 192),
|
||||||
new AudioStream("", MediaFormat.M4A, /**/ 192),
|
generateAudioStream("m4a-192-2", MediaFormat.M4A, 192),
|
||||||
new AudioStream("", MediaFormat.WEBMA, /**/ 192),
|
generateAudioStream("webma-192-3", MediaFormat.WEBMA, 192),
|
||||||
new AudioStream("", MediaFormat.M4A, /**/ 192),
|
generateAudioStream("m4a-192-3", MediaFormat.M4A, 192),
|
||||||
new AudioStream("", MediaFormat.WEBMA, /**/ 192)));
|
generateAudioStream("webma-192-4", MediaFormat.WEBMA, 192)));
|
||||||
// List doesn't contains this format, it should fallback to the highest bitrate audio and
|
// List doesn't contains this format, it should fallback to the highest bitrate audio and
|
||||||
// the highest quality format.
|
// the highest quality format.
|
||||||
stream = testList.get(ListHelper.getHighestQualityAudioIndex(MediaFormat.MP3, testList));
|
stream = testList.get(ListHelper.getHighestQualityAudioIndex(MediaFormat.MP3, testList));
|
||||||
|
@ -250,7 +253,7 @@ public class ListHelperTest {
|
||||||
|
|
||||||
// Adding a new format and bitrate. Adding another stream will have no impact since
|
// Adding a new format and bitrate. Adding another stream will have no impact since
|
||||||
// it's not a preferred format.
|
// it's not a preferred format.
|
||||||
testList.add(new AudioStream("", MediaFormat.WEBMA, /**/ 192));
|
testList.add(generateAudioStream("webma-192-5", MediaFormat.WEBMA, 192));
|
||||||
stream = testList.get(ListHelper.getHighestQualityAudioIndex(MediaFormat.MP3, testList));
|
stream = testList.get(ListHelper.getHighestQualityAudioIndex(MediaFormat.MP3, testList));
|
||||||
assertEquals(192, stream.getAverageBitrate());
|
assertEquals(192, stream.getAverageBitrate());
|
||||||
assertEquals(MediaFormat.M4A, stream.getFormat());
|
assertEquals(MediaFormat.M4A, stream.getFormat());
|
||||||
|
@ -288,8 +291,8 @@ public class ListHelperTest {
|
||||||
////////////////////////////////////////
|
////////////////////////////////////////
|
||||||
|
|
||||||
List<AudioStream> testList = new ArrayList<>(Arrays.asList(
|
List<AudioStream> testList = new ArrayList<>(Arrays.asList(
|
||||||
new AudioStream("", MediaFormat.M4A, /**/ 128),
|
generateAudioStream("m4a-128", MediaFormat.M4A, 128),
|
||||||
new AudioStream("", MediaFormat.WEBMA, /**/ 192)));
|
generateAudioStream("webma-192-1", MediaFormat.WEBMA, 192)));
|
||||||
// List doesn't contains this format
|
// List doesn't contains this format
|
||||||
// It should fallback to the most compact audio no matter what format it is.
|
// It should fallback to the most compact audio no matter what format it is.
|
||||||
AudioStream stream = testList.get(ListHelper.getMostCompactAudioIndex(
|
AudioStream stream = testList.get(ListHelper.getMostCompactAudioIndex(
|
||||||
|
@ -298,7 +301,7 @@ public class ListHelperTest {
|
||||||
assertEquals(MediaFormat.M4A, stream.getFormat());
|
assertEquals(MediaFormat.M4A, stream.getFormat());
|
||||||
|
|
||||||
// WEBMA is more compact than M4A
|
// WEBMA is more compact than M4A
|
||||||
testList.add(new AudioStream("", MediaFormat.WEBMA, /**/ 128));
|
testList.add(generateAudioStream("webma-192-2", MediaFormat.WEBMA, 128));
|
||||||
stream = testList.get(ListHelper.getMostCompactAudioIndex(MediaFormat.MP3, testList));
|
stream = testList.get(ListHelper.getMostCompactAudioIndex(MediaFormat.MP3, testList));
|
||||||
assertEquals(128, stream.getAverageBitrate());
|
assertEquals(128, stream.getAverageBitrate());
|
||||||
assertEquals(MediaFormat.WEBMA, stream.getFormat());
|
assertEquals(MediaFormat.WEBMA, stream.getFormat());
|
||||||
|
@ -308,12 +311,12 @@ public class ListHelperTest {
|
||||||
//////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////
|
||||||
|
|
||||||
testList = new ArrayList<>(Arrays.asList(
|
testList = new ArrayList<>(Arrays.asList(
|
||||||
new AudioStream("", MediaFormat.WEBMA, /**/ 192),
|
generateAudioStream("webma-192-1", MediaFormat.WEBMA, 192),
|
||||||
new AudioStream("", MediaFormat.M4A, /**/ 192),
|
generateAudioStream("m4a-192-1", MediaFormat.M4A, 192),
|
||||||
new AudioStream("", MediaFormat.WEBMA, /**/ 256),
|
generateAudioStream("webma-256", MediaFormat.WEBMA, 256),
|
||||||
new AudioStream("", MediaFormat.M4A, /**/ 192),
|
generateAudioStream("m4a-192-2", MediaFormat.M4A, 192),
|
||||||
new AudioStream("", MediaFormat.WEBMA, /**/ 192),
|
generateAudioStream("webma-192-2", MediaFormat.WEBMA, 192),
|
||||||
new AudioStream("", MediaFormat.M4A, /**/ 192)));
|
generateAudioStream("m4a-192-3", MediaFormat.M4A, 192)));
|
||||||
// List doesn't contain this format
|
// List doesn't contain this format
|
||||||
// It should fallback to the most compact audio no matter what format it is.
|
// It should fallback to the most compact audio no matter what format it is.
|
||||||
stream = testList.get(ListHelper.getMostCompactAudioIndex(MediaFormat.MP3, testList));
|
stream = testList.get(ListHelper.getMostCompactAudioIndex(MediaFormat.MP3, testList));
|
||||||
|
@ -335,14 +338,14 @@ public class ListHelperTest {
|
||||||
@Test
|
@Test
|
||||||
public void getVideoDefaultStreamIndexCombinations() {
|
public void getVideoDefaultStreamIndexCombinations() {
|
||||||
final List<VideoStream> testList = Arrays.asList(
|
final List<VideoStream> testList = Arrays.asList(
|
||||||
new VideoStream("", MediaFormat.MPEG_4, /**/ "1080p"),
|
generateVideoStream("mpeg_4-1080", MediaFormat.MPEG_4, "1080p", false),
|
||||||
new VideoStream("", MediaFormat.MPEG_4, /**/ "720p60"),
|
generateVideoStream("mpeg_4-720_60", MediaFormat.MPEG_4, "720p60", false),
|
||||||
new VideoStream("", MediaFormat.MPEG_4, /**/ "720p"),
|
generateVideoStream("mpeg_4-720", MediaFormat.MPEG_4, "720p", false),
|
||||||
new VideoStream("", MediaFormat.WEBM, /**/ "480p"),
|
generateVideoStream("webm-480", MediaFormat.WEBM, "480p", false),
|
||||||
new VideoStream("", MediaFormat.MPEG_4, /**/ "360p"),
|
generateVideoStream("mpeg_4-360", MediaFormat.MPEG_4, "360p", false),
|
||||||
new VideoStream("", MediaFormat.WEBM, /**/ "360p"),
|
generateVideoStream("webm-360", MediaFormat.WEBM, "360p", false),
|
||||||
new VideoStream("", MediaFormat.v3GPP, /**/ "240p60"),
|
generateVideoStream("v3gpp-240_60", MediaFormat.v3GPP, "240p60", false),
|
||||||
new VideoStream("", MediaFormat.WEBM, /**/ "144p"));
|
generateVideoStream("webm-144", MediaFormat.WEBM, "144p", false));
|
||||||
|
|
||||||
// exact matches
|
// exact matches
|
||||||
assertEquals(1, ListHelper.getVideoStreamIndex("720p60", MediaFormat.MPEG_4, testList));
|
assertEquals(1, ListHelper.getVideoStreamIndex("720p60", MediaFormat.MPEG_4, testList));
|
||||||
|
@ -375,4 +378,30 @@ public class ListHelperTest {
|
||||||
// Can't find a match
|
// Can't find a match
|
||||||
assertEquals(-1, ListHelper.getVideoStreamIndex("100p", null, testList));
|
assertEquals(-1, ListHelper.getVideoStreamIndex("100p", null, testList));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private static AudioStream generateAudioStream(@NonNull final String id,
|
||||||
|
@Nullable final MediaFormat mediaFormat,
|
||||||
|
final int averageBitrate) {
|
||||||
|
return new AudioStream.Builder()
|
||||||
|
.setId(id)
|
||||||
|
.setContent("", true)
|
||||||
|
.setMediaFormat(mediaFormat)
|
||||||
|
.setAverageBitrate(averageBitrate)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private static VideoStream generateVideoStream(@NonNull final String id,
|
||||||
|
@Nullable final MediaFormat mediaFormat,
|
||||||
|
@NonNull final String resolution,
|
||||||
|
final boolean isVideoOnly) {
|
||||||
|
return new VideoStream.Builder()
|
||||||
|
.setId(id)
|
||||||
|
.setContent("", true)
|
||||||
|
.setIsVideoOnly(isVideoOnly)
|
||||||
|
.setResolution(resolution)
|
||||||
|
.setMediaFormat(mediaFormat)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue