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