Merge d134219da5
into 58ae7fbccb
This commit is contained in:
commit
1e71ffc085
|
@ -42,8 +42,6 @@ import static org.schabi.newpipe.player.notification.NotificationConstants.ACTIO
|
||||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_RECREATE_NOTIFICATION;
|
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_RECREATE_NOTIFICATION;
|
||||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_REPEAT;
|
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_REPEAT;
|
||||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_SHUFFLE;
|
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_SHUFFLE;
|
||||||
import static org.schabi.newpipe.util.ListHelper.getPopupResolutionIndex;
|
|
||||||
import static org.schabi.newpipe.util.ListHelper.getResolutionIndex;
|
|
||||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||||
|
|
||||||
|
@ -116,7 +114,6 @@ import org.schabi.newpipe.player.ui.PlayerUiList;
|
||||||
import org.schabi.newpipe.player.ui.PopupPlayerUi;
|
import org.schabi.newpipe.player.ui.PopupPlayerUi;
|
||||||
import org.schabi.newpipe.player.ui.VideoPlayerUi;
|
import org.schabi.newpipe.player.ui.VideoPlayerUi;
|
||||||
import org.schabi.newpipe.util.DependentPreferenceHelper;
|
import org.schabi.newpipe.util.DependentPreferenceHelper;
|
||||||
import org.schabi.newpipe.util.ListHelper;
|
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.SerializedCache;
|
import org.schabi.newpipe.util.SerializedCache;
|
||||||
|
@ -292,7 +289,7 @@ public final class Player implements PlaybackListener, Listener {
|
||||||
context.getString(
|
context.getString(
|
||||||
R.string.use_exoplayer_decoder_fallback_key), false));
|
R.string.use_exoplayer_decoder_fallback_key), false));
|
||||||
|
|
||||||
videoResolver = new VideoPlaybackResolver(context, dataSource, getQualityResolver());
|
videoResolver = new VideoPlaybackResolver(context, dataSource);
|
||||||
audioResolver = new AudioPlaybackResolver(context, dataSource);
|
audioResolver = new AudioPlaybackResolver(context, dataSource);
|
||||||
|
|
||||||
currentThumbnailTarget = getCurrentThumbnailTarget();
|
currentThumbnailTarget = getCurrentThumbnailTarget();
|
||||||
|
@ -306,25 +303,6 @@ public final class Player implements PlaybackListener, Listener {
|
||||||
new NotificationPlayerUi(this)
|
new NotificationPlayerUi(this)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private VideoPlaybackResolver.QualityResolver getQualityResolver() {
|
|
||||||
return new VideoPlaybackResolver.QualityResolver() {
|
|
||||||
@Override
|
|
||||||
public int getDefaultResolutionIndex(final List<VideoStream> sortedVideos) {
|
|
||||||
return videoPlayerSelected()
|
|
||||||
? ListHelper.getDefaultResolutionIndex(context, sortedVideos)
|
|
||||||
: ListHelper.getPopupDefaultResolutionIndex(context, sortedVideos);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getOverrideResolutionIndex(final List<VideoStream> sortedVideos,
|
|
||||||
final String playbackQuality) {
|
|
||||||
return videoPlayerSelected()
|
|
||||||
? getResolutionIndex(context, sortedVideos, playbackQuality)
|
|
||||||
: getPopupResolutionIndex(context, sortedVideos, playbackQuality);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
//endregion
|
//endregion
|
||||||
|
|
||||||
|
|
||||||
|
@ -1908,7 +1886,10 @@ public final class Player implements PlaybackListener, Listener {
|
||||||
// Note that the video is not fetched when the app is in background because the video
|
// Note that the video is not fetched when the app is in background because the video
|
||||||
// renderer is fully disabled (see useVideoSource method), except for HLS streams
|
// renderer is fully disabled (see useVideoSource method), except for HLS streams
|
||||||
// (see https://github.com/google/ExoPlayer/issues/9282).
|
// (see https://github.com/google/ExoPlayer/issues/9282).
|
||||||
return videoResolver.resolve(info);
|
return videoResolver.resolve(info, videoPlayerSelected()
|
||||||
|
? VideoPlaybackResolver.SelectedPlayer.MAIN
|
||||||
|
: VideoPlaybackResolver.SelectedPlayer.POPUP
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void disablePreloadingOfCurrentTrack() {
|
public void disablePreloadingOfCurrentTrack() {
|
||||||
|
|
|
@ -106,19 +106,28 @@ public interface MediaItemTag {
|
||||||
final class Quality {
|
final class Quality {
|
||||||
@NonNull
|
@NonNull
|
||||||
private final List<VideoStream> sortedVideoStreams;
|
private final List<VideoStream> sortedVideoStreams;
|
||||||
|
|
||||||
|
/** Invariant: Index exists in sortedVideoStreams. */
|
||||||
private final int selectedVideoStreamIndex;
|
private final int selectedVideoStreamIndex;
|
||||||
|
|
||||||
private Quality(@NonNull final List<VideoStream> sortedVideoStreams,
|
|
||||||
|
/** Create a new video Quality. The index must be valid in `sortedVideoStreams`.
|
||||||
|
*
|
||||||
|
* @param sortedVideoStreams
|
||||||
|
* @param selectedVideoStreamIndex
|
||||||
|
* @throws ArrayIndexOutOfBoundsException if index does not exist in `sortedVideoStreams`
|
||||||
|
*/
|
||||||
|
public Quality(@NonNull final List<VideoStream> sortedVideoStreams,
|
||||||
final int selectedVideoStreamIndex) {
|
final int selectedVideoStreamIndex) {
|
||||||
|
if (selectedVideoStreamIndex < 0
|
||||||
|
|| selectedVideoStreamIndex >= sortedVideoStreams.size()) {
|
||||||
|
throw new ArrayIndexOutOfBoundsException(
|
||||||
|
"selectedVideoStreamIndex does not exist in sortedVideoStreams");
|
||||||
|
}
|
||||||
this.sortedVideoStreams = sortedVideoStreams;
|
this.sortedVideoStreams = sortedVideoStreams;
|
||||||
this.selectedVideoStreamIndex = selectedVideoStreamIndex;
|
this.selectedVideoStreamIndex = selectedVideoStreamIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Quality of(@NonNull final List<VideoStream> sortedVideoStreams,
|
|
||||||
final int selectedVideoStreamIndex) {
|
|
||||||
return new Quality(sortedVideoStreams, selectedVideoStreamIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public List<VideoStream> getSortedVideoStreams() {
|
public List<VideoStream> getSortedVideoStreams() {
|
||||||
return sortedVideoStreams;
|
return sortedVideoStreams;
|
||||||
|
@ -128,30 +137,35 @@ public interface MediaItemTag {
|
||||||
return selectedVideoStreamIndex;
|
return selectedVideoStreamIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@NonNull
|
||||||
public VideoStream getSelectedVideoStream() {
|
public VideoStream getSelectedVideoStream() {
|
||||||
return selectedVideoStreamIndex < 0
|
return sortedVideoStreams.get(selectedVideoStreamIndex);
|
||||||
|| selectedVideoStreamIndex >= sortedVideoStreams.size()
|
|
||||||
? null : sortedVideoStreams.get(selectedVideoStreamIndex);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class AudioTrack {
|
final class AudioTrack {
|
||||||
@NonNull
|
@NonNull
|
||||||
private final List<AudioStream> audioStreams;
|
private final List<AudioStream> audioStreams;
|
||||||
|
/** Invariant: Index exists in audioStreams. */
|
||||||
private final int selectedAudioStreamIndex;
|
private final int selectedAudioStreamIndex;
|
||||||
|
|
||||||
private AudioTrack(@NonNull final List<AudioStream> audioStreams,
|
/** Create a new AudioTrack. The index must be valid in `audioStreams`.
|
||||||
|
*
|
||||||
|
* @param audioStreams
|
||||||
|
* @param selectedAudioStreamIndex
|
||||||
|
* @throws ArrayIndexOutOfBoundsException if index does not exist in audioStreams.
|
||||||
|
*/
|
||||||
|
public AudioTrack(@NonNull final List<AudioStream> audioStreams,
|
||||||
final int selectedAudioStreamIndex) {
|
final int selectedAudioStreamIndex) {
|
||||||
|
if (selectedAudioStreamIndex < 0
|
||||||
|
|| selectedAudioStreamIndex >= audioStreams.size()) {
|
||||||
|
throw new ArrayIndexOutOfBoundsException(
|
||||||
|
"selectedAudioStreamIndex does not exist in audioStreams");
|
||||||
|
}
|
||||||
this.audioStreams = audioStreams;
|
this.audioStreams = audioStreams;
|
||||||
this.selectedAudioStreamIndex = selectedAudioStreamIndex;
|
this.selectedAudioStreamIndex = selectedAudioStreamIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
static AudioTrack of(@NonNull final List<AudioStream> audioStreams,
|
|
||||||
final int selectedAudioStreamIndex) {
|
|
||||||
return new AudioTrack(audioStreams, selectedAudioStreamIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public List<AudioStream> getAudioStreams() {
|
public List<AudioStream> getAudioStreams() {
|
||||||
return audioStreams;
|
return audioStreams;
|
||||||
|
@ -161,11 +175,9 @@ public interface MediaItemTag {
|
||||||
return selectedAudioStreamIndex;
|
return selectedAudioStreamIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@NonNull
|
||||||
public AudioStream getSelectedAudioStream() {
|
public AudioStream getSelectedAudioStream() {
|
||||||
return selectedAudioStreamIndex < 0
|
return audioStreams.get(selectedAudioStreamIndex);
|
||||||
|| selectedAudioStreamIndex >= audioStreams.size()
|
|
||||||
? null : audioStreams.get(selectedAudioStreamIndex);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,10 +2,8 @@ package org.schabi.newpipe.player.mediaitem;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.MediaItem;
|
import com.google.android.exoplayer2.MediaItem;
|
||||||
|
|
||||||
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.StreamType;
|
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
|
||||||
import org.schabi.newpipe.util.image.ImageStrategy;
|
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
@ -31,7 +29,7 @@ public final class StreamInfoTag implements MediaItemTag {
|
||||||
@Nullable
|
@Nullable
|
||||||
private final Object extras;
|
private final Object extras;
|
||||||
|
|
||||||
private StreamInfoTag(@NonNull final StreamInfo streamInfo,
|
public StreamInfoTag(@NonNull final StreamInfo streamInfo,
|
||||||
@Nullable final MediaItemTag.Quality quality,
|
@Nullable final MediaItemTag.Quality quality,
|
||||||
@Nullable final MediaItemTag.AudioTrack audioTrack,
|
@Nullable final MediaItemTag.AudioTrack audioTrack,
|
||||||
@Nullable final Object extras) {
|
@Nullable final Object extras) {
|
||||||
|
@ -41,29 +39,6 @@ public final class StreamInfoTag implements MediaItemTag {
|
||||||
this.extras = extras;
|
this.extras = extras;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static StreamInfoTag of(@NonNull final StreamInfo streamInfo,
|
|
||||||
@NonNull final List<VideoStream> sortedVideoStreams,
|
|
||||||
final int selectedVideoStreamIndex,
|
|
||||||
@NonNull final List<AudioStream> audioStreams,
|
|
||||||
final int selectedAudioStreamIndex) {
|
|
||||||
final Quality quality = Quality.of(sortedVideoStreams, selectedVideoStreamIndex);
|
|
||||||
final AudioTrack audioTrack =
|
|
||||||
AudioTrack.of(audioStreams, selectedAudioStreamIndex);
|
|
||||||
return new StreamInfoTag(streamInfo, quality, audioTrack, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static StreamInfoTag of(@NonNull final StreamInfo streamInfo,
|
|
||||||
@NonNull final List<AudioStream> audioStreams,
|
|
||||||
final int selectedAudioStreamIndex) {
|
|
||||||
final AudioTrack audioTrack =
|
|
||||||
AudioTrack.of(audioStreams, selectedAudioStreamIndex);
|
|
||||||
return new StreamInfoTag(streamInfo, null, audioTrack, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static StreamInfoTag of(@NonNull final StreamInfo streamInfo) {
|
|
||||||
return new StreamInfoTag(streamInfo, null, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Exception> getErrors() {
|
public List<Exception> getErrors() {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
|
|
|
@ -20,7 +20,9 @@ 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.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
public class AudioPlaybackResolver implements PlaybackResolver {
|
public class AudioPlaybackResolver implements PlaybackResolver {
|
||||||
private static final String TAG = AudioPlaybackResolver.class.getSimpleName();
|
private static final String TAG = AudioPlaybackResolver.class.getSimpleName();
|
||||||
|
@ -45,7 +47,6 @@ public class AudioPlaybackResolver implements PlaybackResolver {
|
||||||
* @param info of the stream
|
* @param info of the stream
|
||||||
* @return the audio source to use or null if none could be found
|
* @return the audio source to use or null if none could be found
|
||||||
*/
|
*/
|
||||||
@Override
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public MediaSource resolve(@NonNull final StreamInfo info) {
|
public MediaSource resolve(@NonNull final StreamInfo info) {
|
||||||
final MediaSource liveSource = PlaybackResolver.maybeBuildLiveMediaSource(dataSource, info);
|
final MediaSource liveSource = PlaybackResolver.maybeBuildLiveMediaSource(dataSource, info);
|
||||||
|
@ -54,22 +55,33 @@ public class AudioPlaybackResolver implements PlaybackResolver {
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<AudioStream> audioStreams =
|
final List<AudioStream> audioStreams =
|
||||||
getFilteredAudioStreams(context, info.getAudioStreams());
|
getFilteredAudioStreams(
|
||||||
|
context,
|
||||||
|
// TODO: getAudioStreams should be @NonNull
|
||||||
|
Objects.requireNonNullElse(info.getAudioStreams(), Collections.emptyList())
|
||||||
|
);
|
||||||
final Stream stream;
|
final Stream stream;
|
||||||
final MediaItemTag tag;
|
final MediaItemTag tag;
|
||||||
|
|
||||||
if (!audioStreams.isEmpty()) {
|
if (!audioStreams.isEmpty()) {
|
||||||
final int audioIndex =
|
final int audioIndex =
|
||||||
ListHelper.getAudioFormatIndex(context, audioStreams, audioTrack);
|
ListHelper.getAudioFormatIndex(context, audioStreams, audioTrack);
|
||||||
stream = getStreamForIndex(audioIndex, audioStreams);
|
assert audioIndex != -1;
|
||||||
tag = StreamInfoTag.of(info, audioStreams, audioIndex);
|
final MediaItemTag.AudioTrack audio =
|
||||||
|
new MediaItemTag.AudioTrack(audioStreams, audioIndex);
|
||||||
|
tag = new StreamInfoTag(info, null, audio, null);
|
||||||
|
stream = audio.getSelectedAudioStream();
|
||||||
} else {
|
} else {
|
||||||
final List<VideoStream> videoStreams =
|
final List<VideoStream> videoStreams =
|
||||||
getPlayableStreams(info.getVideoStreams(), info.getServiceId());
|
getPlayableStreams(info.getVideoStreams(), info.getServiceId());
|
||||||
if (!videoStreams.isEmpty()) {
|
if (!videoStreams.isEmpty()) {
|
||||||
final int index = ListHelper.getDefaultResolutionIndex(context, videoStreams);
|
final int videoIndex = ListHelper.getDefaultResolutionIndex(context, videoStreams);
|
||||||
stream = getStreamForIndex(index, videoStreams);
|
assert videoIndex != -1;
|
||||||
tag = StreamInfoTag.of(info);
|
final MediaItemTag.Quality video =
|
||||||
|
new MediaItemTag.Quality(videoStreams, videoIndex);
|
||||||
|
// why are we not passing `video` as quality here?
|
||||||
|
tag = new StreamInfoTag(info, null, null, null);
|
||||||
|
stream = video.getSelectedVideoStream();
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -84,20 +96,11 @@ public class AudioPlaybackResolver implements PlaybackResolver {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
/** Set audio track to be used the next time {@link #resolve(StreamInfo)} is called.
|
||||||
Stream getStreamForIndex(final int index, @NonNull final List<? extends Stream> streams) {
|
*
|
||||||
if (index >= 0 && index < streams.size()) {
|
* @param audioTrack the {@link AudioStream} audioTrackId that should be selected on resolve
|
||||||
return streams.get(index);
|
*/
|
||||||
}
|
public void setAudioTrack(@Nullable final String audioTrack) {
|
||||||
return null;
|
this.audioTrack = audioTrack;
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public String getAudioTrack() {
|
|
||||||
return audioTrack;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAudioTrack(@Nullable final String audioLanguage) {
|
|
||||||
this.audioTrack = audioLanguage;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,11 +46,10 @@ import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This interface is just a shorthand for {@link Resolver} with {@link StreamInfo} as source and
|
* This interface contains many static methods that can be used by classes
|
||||||
* {@link MediaSource} as product. It contains many static methods that can be used by classes
|
|
||||||
* implementing this interface, and nothing else.
|
* implementing this interface, and nothing else.
|
||||||
*/
|
*/
|
||||||
public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
|
public interface PlaybackResolver {
|
||||||
String TAG = PlaybackResolver.class.getSimpleName();
|
String TAG = PlaybackResolver.class.getSimpleName();
|
||||||
|
|
||||||
|
|
||||||
|
@ -200,7 +199,7 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final StreamInfoTag tag = StreamInfoTag.of(info);
|
final StreamInfoTag tag = new StreamInfoTag(info, null, null, null);
|
||||||
if (!info.getHlsUrl().isEmpty()) {
|
if (!info.getHlsUrl().isEmpty()) {
|
||||||
return buildLiveMediaSource(dataSource, info.getHlsUrl(), C.CONTENT_TYPE_HLS, tag);
|
return buildLiveMediaSource(dataSource, info.getHlsUrl(), C.CONTENT_TYPE_HLS, tag);
|
||||||
} else if (!info.getDashMpdUrl().isEmpty()) {
|
} else if (!info.getDashMpdUrl().isEmpty()) {
|
||||||
|
@ -416,6 +415,7 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
|
||||||
// (which is the last segment of the stream)
|
// (which is the last segment of the stream)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// We know that itagItem has to be set, because it’s youtube-specific
|
||||||
final ItagItem itagItem = Objects.requireNonNull(stream.getItagItem());
|
final ItagItem itagItem = Objects.requireNonNull(stream.getItagItem());
|
||||||
final String manifestString = YoutubePostLiveStreamDvrDashManifestCreator
|
final String manifestString = YoutubePostLiveStreamDvrDashManifestCreator
|
||||||
.fromPostLiveStreamDvrStreamingUrl(stream.getContent(),
|
.fromPostLiveStreamDvrStreamingUrl(stream.getContent(),
|
||||||
|
@ -449,6 +449,7 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
|
||||||
try {
|
try {
|
||||||
final String manifestString = YoutubeProgressiveDashManifestCreator
|
final String manifestString = YoutubeProgressiveDashManifestCreator
|
||||||
.fromProgressiveStreamingUrl(stream.getContent(),
|
.fromProgressiveStreamingUrl(stream.getContent(),
|
||||||
|
// We know that itagItem has to be set, because it’s youtube-specific
|
||||||
Objects.requireNonNull(stream.getItagItem()),
|
Objects.requireNonNull(stream.getItagItem()),
|
||||||
streamInfo.getDuration());
|
streamInfo.getDuration());
|
||||||
return buildYoutubeManualDashMediaSource(dataSource,
|
return buildYoutubeManualDashMediaSource(dataSource,
|
||||||
|
@ -476,6 +477,7 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
|
||||||
try {
|
try {
|
||||||
final String manifestString = YoutubeOtfDashManifestCreator
|
final String manifestString = YoutubeOtfDashManifestCreator
|
||||||
.fromOtfStreamingUrl(stream.getContent(),
|
.fromOtfStreamingUrl(stream.getContent(),
|
||||||
|
// We know that itagItem has to be set, because it’s youtube-specific
|
||||||
Objects.requireNonNull(stream.getItagItem()),
|
Objects.requireNonNull(stream.getItagItem()),
|
||||||
streamInfo.getDuration());
|
streamInfo.getDuration());
|
||||||
return buildYoutubeManualDashMediaSource(dataSource,
|
return buildYoutubeManualDashMediaSource(dataSource,
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
package org.schabi.newpipe.player.resolver;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
public interface Resolver<Source, Product> {
|
|
||||||
@Nullable
|
|
||||||
Product resolve(@NonNull Source source);
|
|
||||||
}
|
|
|
@ -24,7 +24,9 @@ import org.schabi.newpipe.player.mediaitem.StreamInfoTag;
|
||||||
import org.schabi.newpipe.util.ListHelper;
|
import org.schabi.newpipe.util.ListHelper;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
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;
|
||||||
|
@ -39,8 +41,6 @@ public class VideoPlaybackResolver implements PlaybackResolver {
|
||||||
private final Context context;
|
private final Context context;
|
||||||
@NonNull
|
@NonNull
|
||||||
private final PlayerDataSource dataSource;
|
private final PlayerDataSource dataSource;
|
||||||
@NonNull
|
|
||||||
private final QualityResolver qualityResolver;
|
|
||||||
private SourceType streamSourceType;
|
private SourceType streamSourceType;
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
|
@ -54,57 +54,84 @@ public class VideoPlaybackResolver implements PlaybackResolver {
|
||||||
VIDEO_WITH_AUDIO_OR_AUDIO_ONLY
|
VIDEO_WITH_AUDIO_OR_AUDIO_ONLY
|
||||||
}
|
}
|
||||||
|
|
||||||
public VideoPlaybackResolver(@NonNull final Context context,
|
/**
|
||||||
@NonNull final PlayerDataSource dataSource,
|
* Depending on the player we select different video streams.
|
||||||
@NonNull final QualityResolver qualityResolver) {
|
*/
|
||||||
this.context = context;
|
public enum SelectedPlayer {
|
||||||
this.dataSource = dataSource;
|
MAIN,
|
||||||
this.qualityResolver = qualityResolver;
|
POPUP
|
||||||
|
}
|
||||||
|
|
||||||
|
public VideoPlaybackResolver(@NonNull final Context context,
|
||||||
|
@NonNull final PlayerDataSource dataSource) {
|
||||||
|
this.context = context;
|
||||||
|
this.dataSource = dataSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public MediaSource resolve(@NonNull final StreamInfo info) {
|
public MediaSource resolve(@NonNull final StreamInfo info,
|
||||||
|
@NonNull final SelectedPlayer selectedPlayer) {
|
||||||
final MediaSource liveSource = PlaybackResolver.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;
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<MediaSource> mediaSources = new ArrayList<>();
|
|
||||||
|
|
||||||
// Create video stream source
|
// Create video stream source
|
||||||
final List<VideoStream> videoStreamsList = ListHelper.getSortedStreamVideosList(context,
|
final List<VideoStream> videoStreamsList = ListHelper.getSortedStreamVideosList(context,
|
||||||
getPlayableStreams(info.getVideoStreams(), info.getServiceId()),
|
getPlayableStreams(info.getVideoStreams(), info.getServiceId()),
|
||||||
getPlayableStreams(info.getVideoOnlyStreams(), info.getServiceId()), false, true);
|
getPlayableStreams(info.getVideoOnlyStreams(), info.getServiceId()), false, true);
|
||||||
final List<AudioStream> audioStreamsList =
|
final List<AudioStream> audioStreamsList =
|
||||||
getFilteredAudioStreams(context, info.getAudioStreams());
|
getFilteredAudioStreams(
|
||||||
|
context,
|
||||||
|
// TODO: getAudioStreams should be @NonNull
|
||||||
|
Objects.requireNonNullElse(info.getAudioStreams(), Collections.emptyList())
|
||||||
|
);
|
||||||
|
|
||||||
final int videoIndex;
|
int videoIndex = -999;
|
||||||
if (videoStreamsList.isEmpty()) {
|
if (playbackQuality == null) {
|
||||||
videoIndex = -1;
|
switch (selectedPlayer) {
|
||||||
} else if (playbackQuality == null) {
|
case MAIN -> {
|
||||||
videoIndex = qualityResolver.getDefaultResolutionIndex(videoStreamsList);
|
videoIndex = ListHelper.getDefaultResolutionIndex(
|
||||||
|
context,
|
||||||
|
videoStreamsList
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case POPUP -> {
|
||||||
|
videoIndex = ListHelper.getPopupDefaultResolutionIndex(
|
||||||
|
context,
|
||||||
|
videoStreamsList
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
videoIndex = qualityResolver.getOverrideResolutionIndex(videoStreamsList,
|
videoIndex = ListHelper.getDefaultResolutionWithDefaultFormat(
|
||||||
getPlaybackQuality());
|
context,
|
||||||
|
getPlaybackQuality(),
|
||||||
|
videoStreamsList
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final int audioIndex =
|
final int audioIndex =
|
||||||
ListHelper.getAudioFormatIndex(context, audioStreamsList, audioTrack);
|
ListHelper.getAudioFormatIndex(context, audioStreamsList, audioTrack);
|
||||||
final MediaItemTag tag =
|
|
||||||
StreamInfoTag.of(info, videoStreamsList, videoIndex, audioStreamsList, audioIndex);
|
|
||||||
@Nullable final VideoStream video = tag.getMaybeQuality()
|
|
||||||
.map(MediaItemTag.Quality::getSelectedVideoStream)
|
|
||||||
.orElse(null);
|
|
||||||
@Nullable final AudioStream audio = tag.getMaybeAudioTrack()
|
|
||||||
.map(MediaItemTag.AudioTrack::getSelectedAudioStream)
|
|
||||||
.orElse(null);
|
|
||||||
|
|
||||||
|
@Nullable MediaItemTag.Quality video = null;
|
||||||
|
@Nullable MediaItemTag.AudioTrack audio = null;
|
||||||
|
|
||||||
|
if (videoIndex != -1) {
|
||||||
|
video = new MediaItemTag.Quality(videoStreamsList, videoIndex);
|
||||||
|
}
|
||||||
|
if (audioIndex != -1) {
|
||||||
|
audio = new MediaItemTag.AudioTrack(audioStreamsList, audioIndex);
|
||||||
|
}
|
||||||
|
final MediaItemTag tag = new StreamInfoTag(info, video, audio, null);
|
||||||
|
|
||||||
|
final List<MediaSource> mediaSources = new ArrayList<>();
|
||||||
if (video != null) {
|
if (video != null) {
|
||||||
try {
|
try {
|
||||||
|
final VideoStream stream = video.getSelectedVideoStream();
|
||||||
final MediaSource streamSource = PlaybackResolver.buildMediaSource(
|
final MediaSource streamSource = PlaybackResolver.buildMediaSource(
|
||||||
dataSource, video, info, PlaybackResolver.cacheKeyOf(info, video), tag);
|
dataSource, stream, info, PlaybackResolver.cacheKeyOf(info, stream), tag);
|
||||||
mediaSources.add(streamSource);
|
mediaSources.add(streamSource);
|
||||||
} catch (final ResolverException e) {
|
} catch (final ResolverException e) {
|
||||||
Log.e(TAG, "Unable to create video source", e);
|
Log.e(TAG, "Unable to create video source", e);
|
||||||
|
@ -114,10 +141,14 @@ public class VideoPlaybackResolver implements PlaybackResolver {
|
||||||
|
|
||||||
// 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() || audioTrack != null)) {
|
if (audio != null
|
||||||
|
&& (video == null
|
||||||
|
|| video.getSelectedVideoStream().isVideoOnly()
|
||||||
|
|| audioTrack != null)) {
|
||||||
try {
|
try {
|
||||||
|
final AudioStream stream = audio.getSelectedAudioStream();
|
||||||
final MediaSource audioSource = PlaybackResolver.buildMediaSource(
|
final MediaSource audioSource = PlaybackResolver.buildMediaSource(
|
||||||
dataSource, audio, info, PlaybackResolver.cacheKeyOf(info, audio), tag);
|
dataSource, stream, info, PlaybackResolver.cacheKeyOf(info, stream), tag);
|
||||||
mediaSources.add(audioSource);
|
mediaSources.add(audioSource);
|
||||||
streamSourceType = SourceType.VIDEO_WITH_SEPARATED_AUDIO;
|
streamSourceType = SourceType.VIDEO_WITH_SEPARATED_AUDIO;
|
||||||
} catch (final ResolverException e) {
|
} catch (final ResolverException e) {
|
||||||
|
@ -187,18 +218,12 @@ public class VideoPlaybackResolver implements PlaybackResolver {
|
||||||
this.playbackQuality = playbackQuality;
|
this.playbackQuality = playbackQuality;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
/** Set audio track to be used the next time {@link #resolve(StreamInfo, SelectedPlayer)}
|
||||||
public String getAudioTrack() {
|
* is called.
|
||||||
return audioTrack;
|
*
|
||||||
}
|
* @param audioTrack the {@link AudioStream} audioTrackId that should be selected on resolve
|
||||||
|
*/
|
||||||
public void setAudioTrack(@Nullable final String audioLanguage) {
|
public void setAudioTrack(@Nullable final String audioTrack) {
|
||||||
this.audioTrack = audioLanguage;
|
this.audioTrack = audioTrack;
|
||||||
}
|
|
||||||
|
|
||||||
public interface QualityResolver {
|
|
||||||
int getDefaultResolutionIndex(List<VideoStream> sortedVideos);
|
|
||||||
|
|
||||||
int getOverrideResolutionIndex(List<VideoStream> sortedVideos, String playbackQuality);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1054,7 +1054,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
||||||
if (player.getCurrentMetadata() != null
|
if (player.getCurrentMetadata() != null
|
||||||
&& player.getCurrentMetadata().getMaybeQuality().isEmpty()
|
&& player.getCurrentMetadata().getMaybeQuality().isEmpty()
|
||||||
|| (info.getVideoStreams().isEmpty()
|
|| (info.getVideoStreams().isEmpty()
|
||||||
&& info.getVideoOnlyStreams().isEmpty())) {
|
&& info.getVideoOnlyStreams().isEmpty())) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
import android.net.ConnectivityManager;
|
import android.net.ConnectivityManager;
|
||||||
|
import android.util.Pair;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
@ -73,57 +74,32 @@ public final class ListHelper {
|
||||||
/**
|
/**
|
||||||
* @param context Android app context
|
* @param context Android app context
|
||||||
* @param videoStreams list of the video streams to check
|
* @param videoStreams list of the video streams to check
|
||||||
* @return index of the video stream with the default index
|
* @return index of the video stream with the default index, -1 if `videoStreams` is empty
|
||||||
* @see #getDefaultResolutionIndex(String, String, MediaFormat, List)
|
* @see #getDefaultResolutionIndex(String, String, MediaFormat, List)
|
||||||
*/
|
*/
|
||||||
public static int getDefaultResolutionIndex(final Context context,
|
public static int getDefaultResolutionIndex(final Context context,
|
||||||
final List<VideoStream> videoStreams) {
|
final List<VideoStream> videoStreams) {
|
||||||
final String defaultResolution = computeDefaultResolution(context,
|
final String defaultResolution = getPreferredResolutionOrCurrentLimit(context,
|
||||||
R.string.default_resolution_key, R.string.default_resolution_value);
|
R.string.default_resolution_key, R.string.default_resolution_value);
|
||||||
return getDefaultResolutionWithDefaultFormat(context, defaultResolution, videoStreams);
|
return getDefaultResolutionWithDefaultFormat(context, defaultResolution, videoStreams);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param context Android app context
|
|
||||||
* @param videoStreams list of the video streams to check
|
|
||||||
* @param defaultResolution the default resolution to look for
|
|
||||||
* @return index of the video stream with the default index
|
|
||||||
* @see #getDefaultResolutionIndex(String, String, MediaFormat, List)
|
|
||||||
*/
|
|
||||||
public static int getResolutionIndex(final Context context,
|
|
||||||
final List<VideoStream> videoStreams,
|
|
||||||
final String defaultResolution) {
|
|
||||||
return getDefaultResolutionWithDefaultFormat(context, defaultResolution, videoStreams);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param context Android app context
|
* @param context Android app context
|
||||||
* @param videoStreams list of the video streams to check
|
* @param videoStreams list of the video streams to check
|
||||||
* @return index of the video stream with the default index
|
* @return index of the video stream with the default index, -1 if `videoStreams` is empty
|
||||||
* @see #getDefaultResolutionIndex(String, String, MediaFormat, List)
|
* @see #getDefaultResolutionIndex(String, String, MediaFormat, List)
|
||||||
*/
|
*/
|
||||||
public static int getPopupDefaultResolutionIndex(final Context context,
|
public static int getPopupDefaultResolutionIndex(final Context context,
|
||||||
final List<VideoStream> videoStreams) {
|
final List<VideoStream> videoStreams) {
|
||||||
final String defaultResolution = computeDefaultResolution(context,
|
final String defaultResolution = getPreferredResolutionOrCurrentLimit(context,
|
||||||
R.string.default_popup_resolution_key, R.string.default_popup_resolution_value);
|
R.string.default_popup_resolution_key, R.string.default_popup_resolution_value);
|
||||||
return getDefaultResolutionWithDefaultFormat(context, defaultResolution, videoStreams);
|
return getDefaultResolutionWithDefaultFormat(context, defaultResolution, videoStreams);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param context Android app context
|
|
||||||
* @param videoStreams list of the video streams to check
|
|
||||||
* @param defaultResolution the default resolution to look for
|
|
||||||
* @return index of the video stream with the default index
|
|
||||||
* @see #getDefaultResolutionIndex(String, String, MediaFormat, List)
|
|
||||||
*/
|
|
||||||
public static int getPopupResolutionIndex(final Context context,
|
|
||||||
final List<VideoStream> videoStreams,
|
|
||||||
final String defaultResolution) {
|
|
||||||
return getDefaultResolutionWithDefaultFormat(context, defaultResolution, videoStreams);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int getDefaultAudioFormat(final Context context,
|
public static int getDefaultAudioFormat(final Context context,
|
||||||
final List<AudioStream> audioStreams) {
|
@NonNull final List<AudioStream> audioStreams) {
|
||||||
return getAudioIndexByHighestRank(audioStreams,
|
return getAudioIndexByHighestRank(audioStreams,
|
||||||
getAudioTrackComparator(context).thenComparing(getAudioFormatComparator(context)));
|
getAudioTrackComparator(context).thenComparing(getAudioFormatComparator(context)));
|
||||||
}
|
}
|
||||||
|
@ -141,9 +117,17 @@ public final class ListHelper {
|
||||||
return groupedAudioStreams.indexOf(highestRanked);
|
return groupedAudioStreams.indexOf(highestRanked);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Get the index of the audio format to play in audioStreams.
|
||||||
|
* .
|
||||||
|
* @param context
|
||||||
|
* @param audioStreams
|
||||||
|
* @param trackId Try to find this #AudioStream.getAudioTrackId in the audioStreams & select it
|
||||||
|
* @return index to play, or -1 if audioStreams is empty.
|
||||||
|
*/
|
||||||
public static int getAudioFormatIndex(final Context context,
|
public static int getAudioFormatIndex(final Context context,
|
||||||
final List<AudioStream> audioStreams,
|
@NonNull final List<AudioStream> audioStreams,
|
||||||
@Nullable final String trackId) {
|
@Nullable final String trackId) {
|
||||||
|
// if we were given a trackId, try to select that before going to the defaults.
|
||||||
if (trackId != null) {
|
if (trackId != null) {
|
||||||
for (int i = 0; i < audioStreams.size(); i++) {
|
for (int i = 0; i < audioStreams.size(); i++) {
|
||||||
final AudioStream s = audioStreams.get(i);
|
final AudioStream s = audioStreams.get(i);
|
||||||
|
@ -284,44 +268,56 @@ public final class ListHelper {
|
||||||
* Filter the list of audio streams and return a list with the preferred stream for
|
* Filter the list of audio streams and return a list with the preferred stream for
|
||||||
* each audio track. Streams are sorted with the preferred language in the first position.
|
* each audio track. Streams are sorted with the preferred language in the first position.
|
||||||
*
|
*
|
||||||
|
* The following formats cannot be played and are thus skipped:
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>{@literal DeliveryMethod.TORRENT}
|
||||||
|
* <li>both {@literal DeliveryMethod.HLS} AND {@literal MediaFormat.OPUS}</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
* @param context the context to search for the track to give preference
|
* @param context the context to search for the track to give preference
|
||||||
* @param audioStreams the list of audio streams
|
* @param audioStreams the list of audio streams
|
||||||
* @return the sorted, filtered list
|
* @return the sorted, filtered list
|
||||||
*/
|
*/
|
||||||
|
@NonNull
|
||||||
public static List<AudioStream> getFilteredAudioStreams(
|
public static List<AudioStream> getFilteredAudioStreams(
|
||||||
@NonNull final Context context,
|
@NonNull final Context context,
|
||||||
@Nullable final List<AudioStream> audioStreams) {
|
@NonNull final List<AudioStream> audioStreams) {
|
||||||
if (audioStreams == null) {
|
|
||||||
return Collections.emptyList();
|
|
||||||
}
|
|
||||||
|
|
||||||
final HashMap<String, AudioStream> collectedStreams = new HashMap<>();
|
final List<AudioStream> result = audioStreams
|
||||||
|
.stream()
|
||||||
final Comparator<AudioStream> cmp = getAudioFormatComparator(context);
|
// remove torrents we can’t play
|
||||||
|
.filter(stream ->
|
||||||
for (final AudioStream stream : audioStreams) {
|
!(
|
||||||
if (stream.getDeliveryMethod() == DeliveryMethod.TORRENT
|
// we can’t play torrents
|
||||||
|
stream.getDeliveryMethod() == DeliveryMethod.TORRENT
|
||||||
|
// This format is not supported by ExoPlayer when returned as HLS streams,
|
||||||
|
// so we can't play streams using this format and this delivery method.
|
||||||
|| (stream.getDeliveryMethod() == DeliveryMethod.HLS
|
|| (stream.getDeliveryMethod() == DeliveryMethod.HLS
|
||||||
&& stream.getFormat() == MediaFormat.OPUS)) {
|
&& stream.getFormat() == MediaFormat.OPUS))
|
||||||
continue;
|
)
|
||||||
}
|
.collect(Collectors.groupingBy(
|
||||||
|
stream ->
|
||||||
final String trackId = Objects.toString(stream.getAudioTrackId(), "");
|
// Streams grouped by their locale+audiotype
|
||||||
|
// (e.g. en+original, fr+dubbed)
|
||||||
final AudioStream presentStream = collectedStreams.get(trackId);
|
new Pair<Locale, AudioTrackType>(
|
||||||
if (presentStream == null || cmp.compare(stream, presentStream) > 0) {
|
stream.getAudioLocale(),
|
||||||
collectedStreams.put(trackId, stream);
|
stream.getAudioTrackType()
|
||||||
}
|
),
|
||||||
}
|
// from each list of grouped streams,
|
||||||
|
// we select the one that has the most fitting audio type & quality
|
||||||
// Filter unknown audio tracks if there are multiple tracks
|
Collectors.maxBy(getAudioFormatComparator(context))
|
||||||
if (collectedStreams.size() > 1) {
|
))
|
||||||
collectedStreams.remove("");
|
.entrySet()
|
||||||
}
|
.stream()
|
||||||
|
// get streams and remove bins that don’t contain streams
|
||||||
// Sort collected streams by name
|
.flatMap(e -> e.getValue().stream())
|
||||||
return collectedStreams.values().stream().sorted(getAudioTrackNameComparator(context))
|
// sort the preferred audio stream last
|
||||||
|
.sorted(getAudioTrackComparator(context))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
return result;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -390,23 +386,42 @@ public final class ListHelper {
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String computeDefaultResolution(@NonNull final Context context, final int key,
|
/** Lookup the preferred resolution and the current resolution limit.
|
||||||
final int value) {
|
*
|
||||||
|
* @param context App context
|
||||||
|
* @param defaultResolutionKey The defaultResolution preference key
|
||||||
|
* @param defaultResolutionDefaultValue Default resolution if key does not exist
|
||||||
|
* @return The smaller resolution of either the preference or the current limit.
|
||||||
|
*/
|
||||||
|
private static String getPreferredResolutionOrCurrentLimit(
|
||||||
|
@NonNull final Context context,
|
||||||
|
final int defaultResolutionKey,
|
||||||
|
final int defaultResolutionDefaultValue
|
||||||
|
) {
|
||||||
final SharedPreferences preferences =
|
final SharedPreferences preferences =
|
||||||
PreferenceManager.getDefaultSharedPreferences(context);
|
PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
|
|
||||||
// Load the preferred resolution otherwise the best available
|
// Load the preferred resolution otherwise the best available
|
||||||
String resolution = preferences != null
|
final String preferredResolution = preferences.getString(
|
||||||
? preferences.getString(context.getString(key), context.getString(value))
|
context.getString(defaultResolutionKey),
|
||||||
: context.getString(R.string.best_resolution_key);
|
context.getString(defaultResolutionDefaultValue)
|
||||||
|
);
|
||||||
|
|
||||||
final String maxResolution = getResolutionLimit(context);
|
// clamp to the currently maximum allowed resolution
|
||||||
if (maxResolution != null
|
final String result;
|
||||||
&& (resolution.equals(context.getString(R.string.best_resolution_key))
|
final String resolutionLimit = getCurrentResolutionLimit(context);
|
||||||
|| compareVideoStreamResolution(maxResolution, resolution) < 1)) {
|
if (resolutionLimit != null
|
||||||
resolution = maxResolution;
|
&& (
|
||||||
|
// if the preference is best_resolution
|
||||||
|
preferredResolution.equals(context.getString(R.string.best_resolution_key))
|
||||||
|
// or the preference is higher than the current max allowed resolution
|
||||||
|
|| compareVideoStreamResolution(resolutionLimit, preferredResolution) < 1
|
||||||
|
)) {
|
||||||
|
result = resolutionLimit;
|
||||||
|
} else {
|
||||||
|
result = preferredResolution;
|
||||||
}
|
}
|
||||||
return resolution;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -414,17 +429,19 @@ public final class ListHelper {
|
||||||
* on the parameters defaultResolution and defaultFormat.
|
* on the parameters defaultResolution and defaultFormat.
|
||||||
*
|
*
|
||||||
* @param defaultResolution the default resolution to look for
|
* @param defaultResolution the default resolution to look for
|
||||||
|
* (a resolution string or `bestResolutionKey`).
|
||||||
* @param bestResolutionKey key of the best resolution
|
* @param bestResolutionKey key of the best resolution
|
||||||
* @param defaultFormat the default format to look for
|
* @param defaultFormat the default format to look for
|
||||||
* @param videoStreams a mutable list of the video streams to check (it will be sorted in
|
* @param videoStreams a mutable list of the video streams to check (it will be sorted in
|
||||||
* place)
|
* place)
|
||||||
* @return index of the default resolution&format in the sorted videoStreams
|
* @return index of the default resolution&format in the sorted videoStreams,
|
||||||
|
* -1 if `videoStreams` is empty
|
||||||
*/
|
*/
|
||||||
static int getDefaultResolutionIndex(final String defaultResolution,
|
static int getDefaultResolutionIndex(final String defaultResolution,
|
||||||
final String bestResolutionKey,
|
final String bestResolutionKey,
|
||||||
final MediaFormat defaultFormat,
|
final MediaFormat defaultFormat,
|
||||||
@Nullable final List<VideoStream> videoStreams) {
|
@NonNull final List<VideoStream> videoStreams) {
|
||||||
if (videoStreams == null || videoStreams.isEmpty()) {
|
if (videoStreams.isEmpty()) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -539,16 +556,12 @@ public final class ListHelper {
|
||||||
* @param comparator The comparator used for determining the max/best/highest ranked value
|
* @param comparator The comparator used for determining the max/best/highest ranked value
|
||||||
* @return Index of audio stream that produces the highest ranked result or -1 if not found
|
* @return Index of audio stream that produces the highest ranked result or -1 if not found
|
||||||
*/
|
*/
|
||||||
static int getAudioIndexByHighestRank(@Nullable final List<AudioStream> audioStreams,
|
static int getAudioIndexByHighestRank(@NonNull final List<AudioStream> audioStreams,
|
||||||
final Comparator<AudioStream> comparator) {
|
final Comparator<AudioStream> comparator) {
|
||||||
if (audioStreams == null || audioStreams.isEmpty()) {
|
return audioStreams.stream()
|
||||||
return -1;
|
.max(comparator)
|
||||||
}
|
.map(audioStreams::indexOf)
|
||||||
|
.orElse(-1);
|
||||||
final AudioStream highestRankedAudioStream = audioStreams.stream()
|
|
||||||
.max(comparator).orElse(null);
|
|
||||||
|
|
||||||
return audioStreams.indexOf(highestRankedAudioStream);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -627,16 +640,18 @@ public final class ListHelper {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches the desired resolution or returns the default if it is not found.
|
* Fetches the desired resolution or returns the default if it is not found.
|
||||||
* The resolution will be reduced if video chocking is active.
|
* The resolution will be reduced if video choking is active.
|
||||||
*
|
*
|
||||||
* @param context Android app context
|
* @param context Android app context
|
||||||
* @param defaultResolution the default resolution
|
* @param defaultResolution the default resolution
|
||||||
* @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, -1 if `videoStreams` is empty
|
||||||
*/
|
*/
|
||||||
private static int getDefaultResolutionWithDefaultFormat(@NonNull final Context context,
|
public static int getDefaultResolutionWithDefaultFormat(
|
||||||
final String defaultResolution,
|
@NonNull final Context context,
|
||||||
final List<VideoStream> videoStreams) {
|
final String defaultResolution,
|
||||||
|
@NonNull final List<VideoStream> videoStreams
|
||||||
|
) {
|
||||||
final MediaFormat defaultFormat = getDefaultFormat(context,
|
final MediaFormat defaultFormat = getDefaultFormat(context,
|
||||||
R.string.default_video_format_key, R.string.default_video_format_value);
|
R.string.default_video_format_key, R.string.default_video_format_value);
|
||||||
return getDefaultResolutionIndex(defaultResolution,
|
return getDefaultResolutionIndex(defaultResolution,
|
||||||
|
@ -677,6 +692,14 @@ public final class ListHelper {
|
||||||
return format;
|
return format;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** #Comparator for two resolution strings.
|
||||||
|
*
|
||||||
|
* See {@link #sortStreamList} for ordering.
|
||||||
|
*
|
||||||
|
* @param r1 first
|
||||||
|
* @param r2 second
|
||||||
|
* @return comparison int
|
||||||
|
*/
|
||||||
private static int compareVideoStreamResolution(@NonNull final String r1,
|
private static int compareVideoStreamResolution(@NonNull final String r1,
|
||||||
@NonNull final String r2) {
|
@NonNull final String r2) {
|
||||||
try {
|
try {
|
||||||
|
@ -693,31 +716,37 @@ public final class ListHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static boolean isLimitingDataUsage(@NonNull final Context context) {
|
/** Does the application have a maximum resolution set?
|
||||||
return getResolutionLimit(context) != null;
|
*
|
||||||
|
* @param context App context
|
||||||
|
* @return whether a max resolution is set
|
||||||
|
*/
|
||||||
|
static boolean isCurrentlyLimitingDataUsage(@NonNull final Context context) {
|
||||||
|
return getCurrentResolutionLimit(context) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The maximum resolution allowed.
|
* The maximum current resolution allowed by application settings.
|
||||||
|
* Takes into account whether we are on a metered network.
|
||||||
*
|
*
|
||||||
* @param context App context
|
* @param context App context
|
||||||
* @return maximum resolution allowed or null if there is no maximum
|
* @return current maximum resolution allowed or null if there is no maximum
|
||||||
*/
|
*/
|
||||||
private static String getResolutionLimit(@NonNull final Context context) {
|
private static String getCurrentResolutionLimit(@NonNull final Context context) {
|
||||||
String resolutionLimit = null;
|
String currentResolutionLimit = null;
|
||||||
if (isMeteredNetwork(context)) {
|
if (isMeteredNetwork(context)) {
|
||||||
final SharedPreferences preferences =
|
final SharedPreferences preferences =
|
||||||
PreferenceManager.getDefaultSharedPreferences(context);
|
PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
final String defValue = context.getString(R.string.limit_data_usage_none_key);
|
final String defValue = context.getString(R.string.limit_data_usage_none_key);
|
||||||
final String value = preferences.getString(
|
final String value = preferences.getString(
|
||||||
context.getString(R.string.limit_mobile_data_usage_key), defValue);
|
context.getString(R.string.limit_mobile_data_usage_key), defValue);
|
||||||
resolutionLimit = defValue.equals(value) ? null : value;
|
currentResolutionLimit = defValue.equals(value) ? null : value;
|
||||||
}
|
}
|
||||||
return resolutionLimit;
|
return currentResolutionLimit;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current network is metered (like mobile data)?
|
* Is the current network metered (like mobile data)?
|
||||||
*
|
*
|
||||||
* @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
|
||||||
|
@ -744,7 +773,7 @@ public final class ListHelper {
|
||||||
final @NonNull Context context) {
|
final @NonNull Context context) {
|
||||||
final MediaFormat defaultFormat = getDefaultFormat(context,
|
final MediaFormat defaultFormat = getDefaultFormat(context,
|
||||||
R.string.default_audio_format_key, R.string.default_audio_format_value);
|
R.string.default_audio_format_key, R.string.default_audio_format_value);
|
||||||
return getAudioFormatComparator(defaultFormat, isLimitingDataUsage(context));
|
return getAudioFormatComparator(defaultFormat, isCurrentlyLimitingDataUsage(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -872,8 +901,11 @@ public final class ListHelper {
|
||||||
@NonNull final Context context) {
|
@NonNull final Context context) {
|
||||||
final Locale appLoc = Localization.getAppLocale(context);
|
final Locale appLoc = Localization.getAppLocale(context);
|
||||||
|
|
||||||
return Comparator.comparing(AudioStream::getAudioLocale, Comparator.nullsLast(
|
return Comparator.comparing(
|
||||||
Comparator.comparing(locale -> locale.getDisplayName(appLoc))))
|
AudioStream::getAudioLocale,
|
||||||
|
Comparator.nullsLast(
|
||||||
|
Comparator.comparing(locale -> locale.getDisplayName(appLoc))
|
||||||
|
))
|
||||||
.thenComparing(AudioStream::getAudioTrackType);
|
.thenComparing(AudioStream::getAudioTrackType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,7 @@ public class SecondaryStreamHelper<T extends Stream> {
|
||||||
* Finds an audio stream compatible with the provided video-only stream, so that the two streams
|
* Finds an audio stream compatible with the provided video-only stream, so that the two streams
|
||||||
* can be combined in a single file by the downloader. If there are multiple available audio
|
* can be combined in a single file by the downloader. If there are multiple available audio
|
||||||
* streams, chooses either the highest or the lowest quality one based on
|
* streams, chooses either the highest or the lowest quality one based on
|
||||||
* {@link ListHelper#isLimitingDataUsage(Context)}.
|
* {@link ListHelper#isCurrentlyLimitingDataUsage(Context)}.
|
||||||
*
|
*
|
||||||
* @param context Android context
|
* @param context Android context
|
||||||
* @param audioStreams list of audio streams
|
* @param audioStreams list of audio streams
|
||||||
|
@ -56,7 +56,7 @@ public class SecondaryStreamHelper<T extends Stream> {
|
||||||
}
|
}
|
||||||
|
|
||||||
final boolean m4v = mediaFormat == MediaFormat.MPEG_4;
|
final boolean m4v = mediaFormat == MediaFormat.MPEG_4;
|
||||||
final boolean isLimitingDataUsage = ListHelper.isLimitingDataUsage(context);
|
final boolean isLimitingDataUsage = ListHelper.isCurrentlyLimitingDataUsage(context);
|
||||||
|
|
||||||
Comparator<AudioStream> comparator = ListHelper.getAudioFormatComparator(
|
Comparator<AudioStream> comparator = ListHelper.getAudioFormatComparator(
|
||||||
m4v ? MediaFormat.M4A : MediaFormat.WEBMA, isLimitingDataUsage);
|
m4v ? MediaFormat.M4A : MediaFormat.WEBMA, isLimitingDataUsage);
|
||||||
|
|
|
@ -230,7 +230,7 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
|
||||||
new StreamInfoWrapper<>(Collections.emptyList(), null);
|
new StreamInfoWrapper<>(Collections.emptyList(), null);
|
||||||
private static final int SIZE_UNSET = -2;
|
private static final int SIZE_UNSET = -2;
|
||||||
|
|
||||||
private final List<T> streamsList;
|
@NonNull private final List<T> streamsList;
|
||||||
private final long[] streamSizes;
|
private final long[] streamSizes;
|
||||||
private final MediaFormat[] streamFormats;
|
private final MediaFormat[] streamFormats;
|
||||||
private final String unknownSize;
|
private final String unknownSize;
|
||||||
|
@ -432,6 +432,7 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
|
||||||
return (StreamInfoWrapper<X>) EMPTY;
|
return (StreamInfoWrapper<X>) EMPTY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
public List<T> getStreamsList() {
|
public List<T> getStreamsList() {
|
||||||
return streamsList;
|
return streamsList;
|
||||||
}
|
}
|
||||||
|
|
|
@ -279,7 +279,6 @@ public class ListHelperTest {
|
||||||
@Test
|
@Test
|
||||||
public void getHighestQualityAudioNull() {
|
public void getHighestQualityAudioNull() {
|
||||||
final Comparator<AudioStream> cmp = ListHelper.getAudioFormatComparator(null, false);
|
final Comparator<AudioStream> cmp = ListHelper.getAudioFormatComparator(null, false);
|
||||||
assertEquals(-1, ListHelper.getAudioIndexByHighestRank(null, cmp));
|
|
||||||
assertEquals(-1, ListHelper.getAudioIndexByHighestRank(new ArrayList<>(), cmp));
|
assertEquals(-1, ListHelper.getAudioIndexByHighestRank(new ArrayList<>(), cmp));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -356,7 +355,6 @@ public class ListHelperTest {
|
||||||
@Test
|
@Test
|
||||||
public void getLowestQualityAudioNull() {
|
public void getLowestQualityAudioNull() {
|
||||||
final Comparator<AudioStream> cmp = ListHelper.getAudioFormatComparator(null, false);
|
final Comparator<AudioStream> cmp = ListHelper.getAudioFormatComparator(null, false);
|
||||||
assertEquals(-1, ListHelper.getAudioIndexByHighestRank(null, cmp));
|
|
||||||
assertEquals(-1, ListHelper.getAudioIndexByHighestRank(new ArrayList<>(), cmp));
|
assertEquals(-1, ListHelper.getAudioIndexByHighestRank(new ArrayList<>(), cmp));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue