mirror of
				https://github.com/TeamNewPipe/NewPipe
				synced 2025-10-30 23:03:00 +00:00 
			
		
		
		
	Merge pull request #9937 from Theta-Dev/alang-selector
Add support for multiple audio tracks
This commit is contained in:
		| @@ -68,6 +68,8 @@ import org.schabi.newpipe.util.SecondaryStreamHelper; | ||||
| import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener; | ||||
| import org.schabi.newpipe.util.StreamItemAdapter; | ||||
| import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper; | ||||
| import org.schabi.newpipe.util.AudioTrackAdapter; | ||||
| import org.schabi.newpipe.util.AudioTrackAdapter.AudioTracksWrapper; | ||||
| import org.schabi.newpipe.util.ThemeHelper; | ||||
|  | ||||
| import java.io.File; | ||||
| @@ -95,12 +97,14 @@ public class DownloadDialog extends DialogFragment | ||||
|     @State | ||||
|     StreamInfo currentInfo; | ||||
|     @State | ||||
|     StreamSizeWrapper<AudioStream> wrappedAudioStreams; | ||||
|     @State | ||||
|     StreamSizeWrapper<VideoStream> wrappedVideoStreams; | ||||
|     @State | ||||
|     StreamSizeWrapper<SubtitlesStream> wrappedSubtitleStreams; | ||||
|     @State | ||||
|     AudioTracksWrapper wrappedAudioTracks; | ||||
|     @State | ||||
|     int selectedAudioTrackIndex; | ||||
|     @State | ||||
|     int selectedVideoIndex; // set in the constructor | ||||
|     @State | ||||
|     int selectedAudioIndex = 0; // default to the first item | ||||
| @@ -117,6 +121,7 @@ public class DownloadDialog extends DialogFragment | ||||
|     private Context context; | ||||
|     private boolean askForSavePath; | ||||
|  | ||||
|     private AudioTrackAdapter audioTrackAdapter; | ||||
|     private StreamItemAdapter<AudioStream, Stream> audioStreamsAdapter; | ||||
|     private StreamItemAdapter<VideoStream, AudioStream> videoStreamsAdapter; | ||||
|     private StreamItemAdapter<SubtitlesStream, Stream> subtitleStreamsAdapter; | ||||
| @@ -163,18 +168,26 @@ public class DownloadDialog extends DialogFragment | ||||
|     public DownloadDialog(@NonNull final Context context, @NonNull final StreamInfo info) { | ||||
|         this.currentInfo = info; | ||||
|  | ||||
|         final List<AudioStream> audioStreams = | ||||
|                 getStreamsOfSpecifiedDelivery(info.getAudioStreams(), PROGRESSIVE_HTTP); | ||||
|         final List<List<AudioStream>> groupedAudioStreams = | ||||
|                 ListHelper.getGroupedAudioStreams(context, audioStreams); | ||||
|         this.wrappedAudioTracks = new AudioTracksWrapper(groupedAudioStreams, context); | ||||
|         this.selectedAudioTrackIndex = | ||||
|                 ListHelper.getDefaultAudioTrackGroup(context, groupedAudioStreams); | ||||
|  | ||||
|         // 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 | ||||
|                 // If there are multiple languages available, prefer streams without audio | ||||
|                 // to allow language selection | ||||
|                 wrappedAudioTracks.size() > 1 | ||||
|         ); | ||||
|  | ||||
|         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); | ||||
|  | ||||
| @@ -212,33 +225,9 @@ public class DownloadDialog extends DialogFragment | ||||
|         setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context)); | ||||
|         Icepick.restoreInstanceState(this, savedInstanceState); | ||||
|  | ||||
|         final var secondaryStreams = new SparseArrayCompat<SecondaryStreamHelper<AudioStream>>(4); | ||||
|         final List<VideoStream> videoStreams = wrappedVideoStreams.getStreamsList(); | ||||
|  | ||||
|         for (int i = 0; i < videoStreams.size(); i++) { | ||||
|             if (!videoStreams.get(i).isVideoOnly()) { | ||||
|                 continue; | ||||
|             } | ||||
|             final AudioStream audioStream = SecondaryStreamHelper | ||||
|                     .getAudioStreamFor(wrappedAudioStreams.getStreamsList(), videoStreams.get(i)); | ||||
|  | ||||
|             if (audioStream != null) { | ||||
|                 secondaryStreams.append(i, new SecondaryStreamHelper<>(wrappedAudioStreams, | ||||
|                         audioStream)); | ||||
|             } else if (DEBUG) { | ||||
|                 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"); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         this.videoStreamsAdapter = new StreamItemAdapter<>(wrappedVideoStreams, secondaryStreams); | ||||
|         this.audioStreamsAdapter = new StreamItemAdapter<>(wrappedAudioStreams); | ||||
|         this.audioTrackAdapter = new AudioTrackAdapter(wrappedAudioTracks); | ||||
|         this.subtitleStreamsAdapter = new StreamItemAdapter<>(wrappedSubtitleStreams); | ||||
|         updateSecondaryStreams(); | ||||
|  | ||||
|         final Intent intent = new Intent(context, DownloadManagerService.class); | ||||
|         context.startService(intent); | ||||
| @@ -265,6 +254,39 @@ public class DownloadDialog extends DialogFragment | ||||
|         }, Context.BIND_AUTO_CREATE); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Update the displayed video streams based on the selected audio track. | ||||
|      */ | ||||
|     private void updateSecondaryStreams() { | ||||
|         final StreamSizeWrapper<AudioStream> audioStreams = getWrappedAudioStreams(); | ||||
|         final var secondaryStreams = new SparseArrayCompat<SecondaryStreamHelper<AudioStream>>(4); | ||||
|         final List<VideoStream> videoStreams = wrappedVideoStreams.getStreamsList(); | ||||
|         wrappedVideoStreams.resetSizes(); | ||||
|  | ||||
|         for (int i = 0; i < videoStreams.size(); i++) { | ||||
|             if (!videoStreams.get(i).isVideoOnly()) { | ||||
|                 continue; | ||||
|             } | ||||
|             final AudioStream audioStream = SecondaryStreamHelper | ||||
|                     .getAudioStreamFor(audioStreams.getStreamsList(), videoStreams.get(i)); | ||||
|  | ||||
|             if (audioStream != null) { | ||||
|                 secondaryStreams.append(i, new SecondaryStreamHelper<>(audioStreams, audioStream)); | ||||
|             } else if (DEBUG) { | ||||
|                 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"); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         this.videoStreamsAdapter = new StreamItemAdapter<>(wrappedVideoStreams, secondaryStreams); | ||||
|         this.audioStreamsAdapter = new StreamItemAdapter<>(audioStreams); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public View onCreateView(@NonNull final LayoutInflater inflater, | ||||
|                              final ViewGroup container, | ||||
| @@ -285,13 +307,13 @@ public class DownloadDialog extends DialogFragment | ||||
|  | ||||
|         dialogBinding.fileName.setText(FilenameUtils.createFilename(getContext(), | ||||
|                 currentInfo.getName())); | ||||
|         selectedAudioIndex = ListHelper | ||||
|                 .getDefaultAudioFormat(getContext(), wrappedAudioStreams.getStreamsList()); | ||||
|         selectedAudioIndex = ListHelper.getDefaultAudioFormat(getContext(), | ||||
|                 getWrappedAudioStreams().getStreamsList()); | ||||
|  | ||||
|         selectedSubtitleIndex = getSubtitleIndexBy(subtitleStreamsAdapter.getAll()); | ||||
|  | ||||
|         dialogBinding.qualitySpinner.setOnItemSelectedListener(this); | ||||
|  | ||||
|         dialogBinding.audioTrackSpinner.setOnItemSelectedListener(this); | ||||
|         dialogBinding.videoAudioGroup.setOnCheckedChangeListener(this); | ||||
|  | ||||
|         initToolbar(dialogBinding.toolbarLayout.toolbar); | ||||
| @@ -383,7 +405,7 @@ public class DownloadDialog extends DialogFragment | ||||
|                         new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG, | ||||
|                                 "Downloading video stream size", | ||||
|                                 currentInfo.getServiceId())))); | ||||
|         disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedAudioStreams) | ||||
|         disposables.add(StreamSizeWrapper.fetchSizeForWrapper(getWrappedAudioStreams()) | ||||
|                 .subscribe(result -> { | ||||
|                     if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() | ||||
|                             == R.id.audio_button) { | ||||
| @@ -405,14 +427,28 @@ public class DownloadDialog extends DialogFragment | ||||
|                                 currentInfo.getServiceId())))); | ||||
|     } | ||||
|  | ||||
|     private void setupAudioTrackSpinner() { | ||||
|         if (getContext() == null) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         dialogBinding.audioTrackSpinner.setAdapter(audioTrackAdapter); | ||||
|         dialogBinding.audioTrackSpinner.setSelection(selectedAudioTrackIndex); | ||||
|     } | ||||
|  | ||||
|     private void setupAudioSpinner() { | ||||
|         if (getContext() == null) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         dialogBinding.qualitySpinner.setAdapter(audioStreamsAdapter); | ||||
|         dialogBinding.qualitySpinner.setSelection(selectedAudioIndex); | ||||
|         dialogBinding.qualitySpinner.setVisibility(View.GONE); | ||||
|         setRadioButtonsState(true); | ||||
|         dialogBinding.audioStreamSpinner.setAdapter(audioStreamsAdapter); | ||||
|         dialogBinding.audioStreamSpinner.setSelection(selectedAudioIndex); | ||||
|         dialogBinding.audioStreamSpinner.setVisibility(View.VISIBLE); | ||||
|         dialogBinding.audioTrackSpinner.setVisibility( | ||||
|                 wrappedAudioTracks.size() > 1 ? View.VISIBLE : View.GONE); | ||||
|         dialogBinding.audioTrackPresentInVideoText.setVisibility(View.GONE); | ||||
|     } | ||||
|  | ||||
|     private void setupVideoSpinner() { | ||||
| @@ -422,7 +458,19 @@ public class DownloadDialog extends DialogFragment | ||||
|  | ||||
|         dialogBinding.qualitySpinner.setAdapter(videoStreamsAdapter); | ||||
|         dialogBinding.qualitySpinner.setSelection(selectedVideoIndex); | ||||
|         dialogBinding.qualitySpinner.setVisibility(View.VISIBLE); | ||||
|         setRadioButtonsState(true); | ||||
|         dialogBinding.audioStreamSpinner.setVisibility(View.GONE); | ||||
|         onVideoStreamSelected(); | ||||
|     } | ||||
|  | ||||
|     private void onVideoStreamSelected() { | ||||
|         final boolean isVideoOnly = videoStreamsAdapter.getItem(selectedVideoIndex).isVideoOnly(); | ||||
|  | ||||
|         dialogBinding.audioTrackSpinner.setVisibility( | ||||
|                 isVideoOnly && wrappedAudioTracks.size() > 1 ? View.VISIBLE : View.GONE); | ||||
|         dialogBinding.audioTrackPresentInVideoText.setVisibility( | ||||
|                 !isVideoOnly && wrappedAudioTracks.size() > 1 ? View.VISIBLE : View.GONE); | ||||
|     } | ||||
|  | ||||
|     private void setupSubtitleSpinner() { | ||||
| @@ -432,7 +480,11 @@ public class DownloadDialog extends DialogFragment | ||||
|  | ||||
|         dialogBinding.qualitySpinner.setAdapter(subtitleStreamsAdapter); | ||||
|         dialogBinding.qualitySpinner.setSelection(selectedSubtitleIndex); | ||||
|         dialogBinding.qualitySpinner.setVisibility(View.VISIBLE); | ||||
|         setRadioButtonsState(true); | ||||
|         dialogBinding.audioStreamSpinner.setVisibility(View.GONE); | ||||
|         dialogBinding.audioTrackSpinner.setVisibility(View.GONE); | ||||
|         dialogBinding.audioTrackPresentInVideoText.setVisibility(View.GONE); | ||||
|     } | ||||
|  | ||||
|  | ||||
| @@ -550,18 +602,31 @@ public class DownloadDialog extends DialogFragment | ||||
|                     + "parent = [" + parent + "], view = [" + view + "], " | ||||
|                     + "position = [" + position + "], id = [" + id + "]"); | ||||
|         } | ||||
|         switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) { | ||||
|             case R.id.audio_button: | ||||
|  | ||||
|         switch (parent.getId()) { | ||||
|             case R.id.quality_spinner: | ||||
|                 switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) { | ||||
|                     case R.id.video_button: | ||||
|                         selectedVideoIndex = position; | ||||
|                         onVideoStreamSelected(); | ||||
|                         break; | ||||
|                     case R.id.subtitle_button: | ||||
|                         selectedSubtitleIndex = position; | ||||
|                         break; | ||||
|                 } | ||||
|                 onItemSelectedSetFileName(); | ||||
|                 break; | ||||
|             case R.id.audio_track_spinner: | ||||
|                 final boolean trackChanged = selectedAudioTrackIndex != position; | ||||
|                 selectedAudioTrackIndex = position; | ||||
|                 if (trackChanged) { | ||||
|                     updateSecondaryStreams(); | ||||
|                     fetchStreamsSize(); | ||||
|                 } | ||||
|                 break; | ||||
|             case R.id.audio_stream_spinner: | ||||
|                 selectedAudioIndex = position; | ||||
|                 break; | ||||
|             case R.id.video_button: | ||||
|                 selectedVideoIndex = position; | ||||
|                 break; | ||||
|             case R.id.subtitle_button: | ||||
|                 selectedSubtitleIndex = position; | ||||
|                 break; | ||||
|         } | ||||
|         onItemSelectedSetFileName(); | ||||
|     } | ||||
|  | ||||
|     private void onItemSelectedSetFileName() { | ||||
| @@ -607,6 +672,7 @@ public class DownloadDialog extends DialogFragment | ||||
|  | ||||
|     protected void setupDownloadOptions() { | ||||
|         setRadioButtonsState(false); | ||||
|         setupAudioTrackSpinner(); | ||||
|  | ||||
|         final boolean isVideoStreamsAvailable = videoStreamsAdapter.getCount() > 0; | ||||
|         final boolean isAudioStreamsAvailable = audioStreamsAdapter.getCount() > 0; | ||||
| @@ -657,6 +723,13 @@ public class DownloadDialog extends DialogFragment | ||||
|         dialogBinding.subtitleButton.setEnabled(enabled); | ||||
|     } | ||||
|  | ||||
|     private StreamSizeWrapper<AudioStream> getWrappedAudioStreams() { | ||||
|         if (selectedAudioTrackIndex < 0 || selectedAudioTrackIndex > wrappedAudioTracks.size()) { | ||||
|             return StreamSizeWrapper.empty(); | ||||
|         } | ||||
|         return wrappedAudioTracks.getTracksList().get(selectedAudioTrackIndex); | ||||
|     } | ||||
|  | ||||
|     private int getSubtitleIndexBy(@NonNull final List<SubtitlesStream> streams) { | ||||
|         final Localization preferredLocalization = NewPipe.getPreferredLocalization(); | ||||
|  | ||||
| @@ -1013,7 +1086,6 @@ public class DownloadDialog extends DialogFragment | ||||
|                         psName = Postprocessing.ALGORITHM_WEBM_MUXER; | ||||
|                     } | ||||
|  | ||||
|                     psArgs = null; | ||||
|                     final long videoSize = wrappedVideoStreams.getSizeInBytes( | ||||
|                             (VideoStream) selectedStream); | ||||
|  | ||||
|   | ||||
| @@ -162,8 +162,12 @@ public final class VideoDetailFragment | ||||
|     private boolean showRelatedItems; | ||||
|     private boolean showDescription; | ||||
|     private String selectedTabTag; | ||||
|     @AttrRes @NonNull final List<Integer> tabIcons = new ArrayList<>(); | ||||
|     @StringRes @NonNull final List<Integer> tabContentDescriptions = new ArrayList<>(); | ||||
|     @AttrRes | ||||
|     @NonNull | ||||
|     final List<Integer> tabIcons = new ArrayList<>(); | ||||
|     @StringRes | ||||
|     @NonNull | ||||
|     final List<Integer> tabContentDescriptions = new ArrayList<>(); | ||||
|     private boolean tabSettingsChanged = false; | ||||
|     private int lastAppBarVerticalOffset = Integer.MAX_VALUE; // prevents useless updates | ||||
|  | ||||
| @@ -1040,20 +1044,10 @@ public final class VideoDetailFragment | ||||
|             player.setRecovery(); | ||||
|         } | ||||
|  | ||||
|         if (!useExternalAudioPlayer) { | ||||
|             openNormalBackgroundPlayer(append); | ||||
|         if (useExternalAudioPlayer) { | ||||
|             showExternalAudioPlaybackDialog(); | ||||
|         } else { | ||||
|             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)); | ||||
|             openNormalBackgroundPlayer(append); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -1106,7 +1100,7 @@ public final class VideoDetailFragment | ||||
|  | ||||
|         if (PreferenceManager.getDefaultSharedPreferences(activity) | ||||
|                 .getBoolean(this.getString(R.string.use_external_video_player_key), false)) { | ||||
|             showExternalPlaybackDialog(); | ||||
|             showExternalVideoPlaybackDialog(); | ||||
|         } else { | ||||
|             replaceQueueIfUserConfirms(this::openMainPlayer); | ||||
|         } | ||||
| @@ -2112,7 +2106,7 @@ public final class VideoDetailFragment | ||||
|                 }).show(); | ||||
|     } | ||||
|  | ||||
|     private void showExternalPlaybackDialog() { | ||||
|     private void showExternalVideoPlaybackDialog() { | ||||
|         if (currentInfo == null) { | ||||
|             return; | ||||
|         } | ||||
| @@ -2159,6 +2153,44 @@ public final class VideoDetailFragment | ||||
|         builder.show(); | ||||
|     } | ||||
|  | ||||
|     private void showExternalAudioPlaybackDialog() { | ||||
|         if (currentInfo == null) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         final List<AudioStream> audioStreams = getUrlAndNonTorrentStreams( | ||||
|                 currentInfo.getAudioStreams()); | ||||
|         final List<AudioStream> audioTracks = | ||||
|                 ListHelper.getFilteredAudioStreams(activity, audioStreams); | ||||
|  | ||||
|         if (audioTracks.isEmpty()) { | ||||
|             Toast.makeText(activity, R.string.no_audio_streams_available_for_external_players, | ||||
|                     Toast.LENGTH_SHORT).show(); | ||||
|         } else if (audioTracks.size() == 1) { | ||||
|             startOnExternalPlayer(activity, currentInfo, audioTracks.get(0)); | ||||
|         } else { | ||||
|             final AlertDialog.Builder builder = new AlertDialog.Builder(activity); | ||||
|             builder.setTitle(R.string.select_audio_track_external_players); | ||||
|             builder.setNeutralButton(R.string.open_in_browser, (dialog, i) -> | ||||
|                     ShareUtils.openUrlInBrowser(requireActivity(), url)); | ||||
|  | ||||
|             final int selectedAudioStream = | ||||
|                     ListHelper.getDefaultAudioFormat(activity, audioTracks); | ||||
|             final CharSequence[] trackNames = audioTracks.stream() | ||||
|                     .map(audioStream -> Localization.audioTrackName(activity, audioStream)) | ||||
|                     .toArray(CharSequence[]::new); | ||||
|  | ||||
|             builder.setSingleChoiceItems(trackNames, selectedAudioStream, null); | ||||
|             builder.setNegativeButton(R.string.cancel, null); | ||||
|             builder.setPositiveButton(R.string.ok, (dialog, i) -> { | ||||
|                 final int index = ((AlertDialog) dialog).getListView().getCheckedItemPosition(); | ||||
|                 startOnExternalPlayer(activity, currentInfo, | ||||
|                         audioTracks.get(index)); | ||||
|             }); | ||||
|             builder.show(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /* | ||||
|      * Remove unneeded information while waiting for a next task | ||||
|      * */ | ||||
|   | ||||
| @@ -13,6 +13,7 @@ import android.provider.Settings; | ||||
| import android.util.Log; | ||||
| import android.view.Menu; | ||||
| import android.view.MenuItem; | ||||
| import android.view.SubMenu; | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| import android.widget.SeekBar; | ||||
| @@ -27,11 +28,13 @@ import com.google.android.exoplayer2.PlaybackParameters; | ||||
|  | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.databinding.ActivityPlayerQueueControlBinding; | ||||
| import org.schabi.newpipe.extractor.stream.AudioStream; | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||
| import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; | ||||
| import org.schabi.newpipe.local.dialog.PlaylistDialog; | ||||
| import org.schabi.newpipe.player.event.PlayerEventListener; | ||||
| import org.schabi.newpipe.player.helper.PlaybackParameterDialog; | ||||
| import org.schabi.newpipe.player.mediaitem.MediaItemTag; | ||||
| import org.schabi.newpipe.player.playqueue.PlayQueue; | ||||
| import org.schabi.newpipe.player.playqueue.PlayQueueAdapter; | ||||
| import org.schabi.newpipe.player.playqueue.PlayQueueItem; | ||||
| @@ -44,6 +47,9 @@ import org.schabi.newpipe.util.PermissionHelper; | ||||
| import org.schabi.newpipe.util.ServiceHelper; | ||||
| import org.schabi.newpipe.util.ThemeHelper; | ||||
|  | ||||
| import java.util.List; | ||||
| import java.util.Optional; | ||||
|  | ||||
| public final class PlayQueueActivity extends AppCompatActivity | ||||
|         implements PlayerEventListener, SeekBar.OnSeekBarChangeListener, | ||||
|         View.OnClickListener, PlaybackParameterDialog.Callback { | ||||
| @@ -52,6 +58,8 @@ public final class PlayQueueActivity extends AppCompatActivity | ||||
|  | ||||
|     private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80; | ||||
|  | ||||
|     private static final int MENU_ID_AUDIO_TRACK = 71; | ||||
|  | ||||
|     private Player player; | ||||
|  | ||||
|     private boolean serviceBound; | ||||
| @@ -97,6 +105,7 @@ public final class PlayQueueActivity extends AppCompatActivity | ||||
|         this.menu = m; | ||||
|         getMenuInflater().inflate(R.menu.menu_play_queue, m); | ||||
|         getMenuInflater().inflate(R.menu.menu_play_queue_bg, m); | ||||
|         buildAudioTrackMenu(); | ||||
|         onMaybeMuteChanged(); | ||||
|         // to avoid null reference | ||||
|         if (player != null) { | ||||
| @@ -153,6 +162,12 @@ public final class PlayQueueActivity extends AppCompatActivity | ||||
|                 NavigationHelper.playOnBackgroundPlayer(this, player.getPlayQueue(), true); | ||||
|                 return true; | ||||
|         } | ||||
|  | ||||
|         if (item.getGroupId() == MENU_ID_AUDIO_TRACK) { | ||||
|             onAudioTrackClick(item.getItemId()); | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|         return super.onOptionsItemSelected(item); | ||||
|     } | ||||
|  | ||||
| @@ -591,4 +606,69 @@ public final class PlayQueueActivity extends AppCompatActivity | ||||
|             item.setIcon(player.isMuted() ? R.drawable.ic_volume_off : R.drawable.ic_volume_up); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onAudioTrackUpdate() { | ||||
|         buildAudioTrackMenu(); | ||||
|     } | ||||
|  | ||||
|     private void buildAudioTrackMenu() { | ||||
|         if (menu == null) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         final MenuItem audioTrackSelector = menu.findItem(R.id.action_audio_track); | ||||
|         final List<AudioStream> availableStreams = | ||||
|                 Optional.ofNullable(player.getCurrentMetadata()) | ||||
|                         .flatMap(MediaItemTag::getMaybeAudioTrack) | ||||
|                         .map(MediaItemTag.AudioTrack::getAudioStreams) | ||||
|                         .orElse(null); | ||||
|         final Optional<AudioStream> selectedAudioStream = player.getSelectedAudioStream(); | ||||
|  | ||||
|         if (availableStreams == null || availableStreams.size() < 2 | ||||
|                 || selectedAudioStream.isEmpty()) { | ||||
|             audioTrackSelector.setVisible(false); | ||||
|         } else { | ||||
|             final SubMenu audioTrackMenu = audioTrackSelector.getSubMenu(); | ||||
|             audioTrackMenu.clear(); | ||||
|  | ||||
|             for (int i = 0; i < availableStreams.size(); i++) { | ||||
|                 final AudioStream audioStream = availableStreams.get(i); | ||||
|                 audioTrackMenu.add(MENU_ID_AUDIO_TRACK, i, Menu.NONE, | ||||
|                         Localization.audioTrackName(this, audioStream)); | ||||
|             } | ||||
|  | ||||
|             final AudioStream s = selectedAudioStream.get(); | ||||
|             final String trackName = Localization.audioTrackName(this, s); | ||||
|             audioTrackSelector.setTitle( | ||||
|                     getString(R.string.play_queue_audio_track, trackName)); | ||||
|  | ||||
|             final String shortName = s.getAudioLocale() != null | ||||
|                     ? s.getAudioLocale().getLanguage() : trackName; | ||||
|             audioTrackSelector.setTitleCondensed( | ||||
|                     shortName.substring(0, Math.min(shortName.length(), 2))); | ||||
|             audioTrackSelector.setVisible(true); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Called when an item from the audio track selector is selected. | ||||
|      * | ||||
|      * @param itemId index of the selected item | ||||
|      */ | ||||
|     private void onAudioTrackClick(final int itemId) { | ||||
|         if (player.getCurrentMetadata() == null) { | ||||
|             return; | ||||
|         } | ||||
|         player.getCurrentMetadata().getMaybeAudioTrack().ifPresent(audioTrack -> { | ||||
|             final List<AudioStream> availableStreams = audioTrack.getAudioStreams(); | ||||
|             final int selectedStreamIndex = audioTrack.getSelectedAudioStreamIndex(); | ||||
|             if (selectedStreamIndex == itemId || availableStreams.size() <= itemId) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             final String newAudioTrack = availableStreams.get(itemId).getAudioTrackId(); | ||||
|             player.setAudioTrack(newAudioTrack); | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -86,6 +86,7 @@ import org.schabi.newpipe.databinding.PlayerBinding; | ||||
| import org.schabi.newpipe.error.ErrorInfo; | ||||
| import org.schabi.newpipe.error.ErrorUtil; | ||||
| import org.schabi.newpipe.error.UserAction; | ||||
| import org.schabi.newpipe.extractor.stream.AudioStream; | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||
| import org.schabi.newpipe.extractor.stream.StreamType; | ||||
| import org.schabi.newpipe.extractor.stream.VideoStream; | ||||
| @@ -179,13 +180,18 @@ public final class Player implements PlaybackListener, Listener { | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     // play queue might be null e.g. while player is starting | ||||
|     @Nullable private PlayQueue playQueue; | ||||
|     @Nullable | ||||
|     private PlayQueue playQueue; | ||||
|  | ||||
|     @Nullable private MediaSourceManager playQueueManager; | ||||
|     @Nullable | ||||
|     private MediaSourceManager playQueueManager; | ||||
|  | ||||
|     @Nullable private PlayQueueItem currentItem; | ||||
|     @Nullable private MediaItemTag currentMetadata; | ||||
|     @Nullable private Bitmap currentThumbnail; | ||||
|     @Nullable | ||||
|     private PlayQueueItem currentItem; | ||||
|     @Nullable | ||||
|     private MediaItemTag currentMetadata; | ||||
|     @Nullable | ||||
|     private Bitmap currentThumbnail; | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Player | ||||
| @@ -194,12 +200,17 @@ public final class Player implements PlaybackListener, Listener { | ||||
|     private ExoPlayer simpleExoPlayer; | ||||
|     private AudioReactor audioReactor; | ||||
|  | ||||
|     @NonNull private final DefaultTrackSelector trackSelector; | ||||
|     @NonNull private final LoadController loadController; | ||||
|     @NonNull private final DefaultRenderersFactory renderFactory; | ||||
|     @NonNull | ||||
|     private final DefaultTrackSelector trackSelector; | ||||
|     @NonNull | ||||
|     private final LoadController loadController; | ||||
|     @NonNull | ||||
|     private final DefaultRenderersFactory renderFactory; | ||||
|  | ||||
|     @NonNull private final VideoPlaybackResolver videoResolver; | ||||
|     @NonNull private final AudioPlaybackResolver audioResolver; | ||||
|     @NonNull | ||||
|     private final VideoPlaybackResolver videoResolver; | ||||
|     @NonNull | ||||
|     private final AudioPlaybackResolver audioResolver; | ||||
|  | ||||
|     private final PlayerService service; //TODO try to remove and replace everything with context | ||||
|  | ||||
| @@ -224,24 +235,32 @@ public final class Player implements PlaybackListener, Listener { | ||||
|  | ||||
|     private BroadcastReceiver broadcastReceiver; | ||||
|     private IntentFilter intentFilter; | ||||
|     @Nullable private PlayerServiceEventListener fragmentListener = null; | ||||
|     @Nullable private PlayerEventListener activityListener = null; | ||||
|     @Nullable | ||||
|     private PlayerServiceEventListener fragmentListener = null; | ||||
|     @Nullable | ||||
|     private PlayerEventListener activityListener = null; | ||||
|  | ||||
|     @NonNull private final SerialDisposable progressUpdateDisposable = new SerialDisposable(); | ||||
|     @NonNull private final CompositeDisposable databaseUpdateDisposable = new CompositeDisposable(); | ||||
|     @NonNull | ||||
|     private final SerialDisposable progressUpdateDisposable = new SerialDisposable(); | ||||
|     @NonNull | ||||
|     private final CompositeDisposable databaseUpdateDisposable = new CompositeDisposable(); | ||||
|  | ||||
|     // This is the only listener we need for thumbnail loading, since there is always at most only | ||||
|     // one thumbnail being loaded at a time. This field is also here to maintain a strong reference, | ||||
|     // which would otherwise be garbage collected since Picasso holds weak references to targets. | ||||
|     @NonNull private final Target currentThumbnailTarget; | ||||
|     @NonNull | ||||
|     private final Target currentThumbnailTarget; | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Utils | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     @NonNull private final Context context; | ||||
|     @NonNull private final SharedPreferences prefs; | ||||
|     @NonNull private final HistoryRecordManager recordManager; | ||||
|     @NonNull | ||||
|     private final Context context; | ||||
|     @NonNull | ||||
|     private final SharedPreferences prefs; | ||||
|     @NonNull | ||||
|     private final HistoryRecordManager recordManager; | ||||
|  | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
| @@ -333,7 +352,7 @@ public final class Player implements PlaybackListener, Listener { | ||||
|         isAudioOnly = audioPlayerSelected(); | ||||
|  | ||||
|         if (intent.hasExtra(PLAYBACK_QUALITY)) { | ||||
|             setPlaybackQuality(intent.getStringExtra(PLAYBACK_QUALITY)); | ||||
|             videoResolver.setPlaybackQuality(intent.getStringExtra(PLAYBACK_QUALITY)); | ||||
|         } | ||||
|  | ||||
|         // Resolve enqueue intents | ||||
| @@ -341,7 +360,7 @@ public final class Player implements PlaybackListener, Listener { | ||||
|             playQueue.append(newQueue.getStreams()); | ||||
|             return; | ||||
|  | ||||
|         // Resolve enqueue next intents | ||||
|             // Resolve enqueue next intents | ||||
|         } else if (intent.getBooleanExtra(ENQUEUE_NEXT, false) && playQueue != null) { | ||||
|             final int currentIndex = playQueue.getIndex(); | ||||
|             playQueue.append(newQueue.getStreams()); | ||||
| @@ -913,7 +932,7 @@ public final class Player implements PlaybackListener, Listener { | ||||
|  | ||||
|     private Disposable getProgressUpdateDisposable() { | ||||
|         return Observable.interval(PROGRESS_LOOP_INTERVAL_MILLIS, MILLISECONDS, | ||||
|                 AndroidSchedulers.mainThread()) | ||||
|                         AndroidSchedulers.mainThread()) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe(ignored -> triggerProgressUpdate(), | ||||
|                         error -> Log.e(TAG, "Progress update failure: ", error)); | ||||
| @@ -922,7 +941,6 @@ public final class Player implements PlaybackListener, Listener { | ||||
|     //endregion | ||||
|  | ||||
|  | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Playback states | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
| @@ -1244,6 +1262,9 @@ public final class Player implements PlaybackListener, Listener { | ||||
|             } | ||||
|             final StreamInfo previousInfo = Optional.ofNullable(currentMetadata) | ||||
|                     .flatMap(MediaItemTag::getMaybeStreamInfo).orElse(null); | ||||
|             final MediaItemTag.AudioTrack previousAudioTrack = | ||||
|                     Optional.ofNullable(currentMetadata) | ||||
|                             .flatMap(MediaItemTag::getMaybeAudioTrack).orElse(null); | ||||
|             currentMetadata = tag; | ||||
|  | ||||
|             if (!currentMetadata.getErrors().isEmpty()) { | ||||
| @@ -1264,6 +1285,12 @@ public final class Player implements PlaybackListener, Listener { | ||||
|                 if (previousInfo == null || !previousInfo.getUrl().equals(info.getUrl())) { | ||||
|                     // only update with the new stream info if it has actually changed | ||||
|                     updateMetadataWith(info); | ||||
|                 } else if (previousAudioTrack == null | ||||
|                         || tag.getMaybeAudioTrack() | ||||
|                         .map(t -> t.getSelectedAudioStreamIndex() | ||||
|                                 != previousAudioTrack.getSelectedAudioStreamIndex()) | ||||
|                         .orElse(false)) { | ||||
|                     notifyAudioTrackUpdateToListeners(); | ||||
|                 } | ||||
|             }); | ||||
|         }); | ||||
| @@ -1351,6 +1378,7 @@ public final class Player implements PlaybackListener, Listener { | ||||
|     // Errors | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|     //region Errors | ||||
|  | ||||
|     /** | ||||
|      * Process exceptions produced by {@link com.google.android.exoplayer2.ExoPlayer ExoPlayer}. | ||||
|      * <p>There are multiple types of errors:</p> | ||||
| @@ -1377,8 +1405,9 @@ public final class Player implements PlaybackListener, Listener { | ||||
|      * For any error above that is <b>not</b> explicitly <b>catchable</b>, the player will | ||||
|      * create a notification so users are aware. | ||||
|      * </ul> | ||||
|      * | ||||
|      * @see com.google.android.exoplayer2.Player.Listener#onPlayerError(PlaybackException) | ||||
|      * */ | ||||
|      */ | ||||
|     // Any error code not explicitly covered here are either unrelated to NewPipe use case | ||||
|     // (e.g. DRM) or not recoverable (e.g. Decoder error). In both cases, the player should | ||||
|     // shutdown. | ||||
| @@ -1760,6 +1789,7 @@ public final class Player implements PlaybackListener, Listener { | ||||
|         registerStreamViewed(); | ||||
|  | ||||
|         notifyMetadataUpdateToListeners(); | ||||
|         notifyAudioTrackUpdateToListeners(); | ||||
|         UIs.call(playerUi -> playerUi.onMetadataChanged(info)); | ||||
|     } | ||||
|  | ||||
| @@ -1888,6 +1918,12 @@ public final class Player implements PlaybackListener, Listener { | ||||
|                 .map(quality -> quality.getSortedVideoStreams() | ||||
|                         .get(quality.getSelectedVideoStreamIndex())); | ||||
|     } | ||||
|  | ||||
|     public Optional<AudioStream> getSelectedAudioStream() { | ||||
|         return Optional.ofNullable(currentMetadata) | ||||
|                 .flatMap(MediaItemTag::getMaybeAudioTrack) | ||||
|                 .map(MediaItemTag.AudioTrack::getSelectedAudioStream); | ||||
|     } | ||||
|     //endregion | ||||
|  | ||||
|  | ||||
| @@ -2019,6 +2055,15 @@ public final class Player implements PlaybackListener, Listener { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void notifyAudioTrackUpdateToListeners() { | ||||
|         if (fragmentListener != null) { | ||||
|             fragmentListener.onAudioTrackUpdate(); | ||||
|         } | ||||
|         if (activityListener != null) { | ||||
|             activityListener.onAudioTrackUpdate(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void useVideoSource(final boolean videoEnabled) { | ||||
|         if (playQueue == null || isAudioOnly == !videoEnabled || audioPlayerSelected()) { | ||||
|             return; | ||||
| @@ -2115,7 +2160,7 @@ public final class Player implements PlaybackListener, Listener { | ||||
|         // because the stream source will be probably the same as the current played | ||||
|         if (sourceType == SourceType.VIDEO_WITH_SEPARATED_AUDIO | ||||
|                 || (sourceType == SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY | ||||
|                     && isNullOrEmpty(streamInfo.getAudioStreams()))) { | ||||
|                 && isNullOrEmpty(streamInfo.getAudioStreams()))) { | ||||
|             // It's not needed to reload the play queue manager only if the content's stream type | ||||
|             // is a video stream, a live stream or an ended live stream | ||||
|             return !StreamTypeUtil.isVideo(streamType); | ||||
| @@ -2177,7 +2222,18 @@ public final class Player implements PlaybackListener, Listener { | ||||
|     } | ||||
|  | ||||
|     public void setPlaybackQuality(@Nullable final String quality) { | ||||
|         saveStreamProgressState(); | ||||
|         setRecovery(); | ||||
|         videoResolver.setPlaybackQuality(quality); | ||||
|         reloadPlayQueueManager(); | ||||
|     } | ||||
|  | ||||
|     public void setAudioTrack(@Nullable final String audioTrackId) { | ||||
|         saveStreamProgressState(); | ||||
|         setRecovery(); | ||||
|         videoResolver.setAudioTrack(audioTrackId); | ||||
|         audioResolver.setAudioTrack(audioTrackId); | ||||
|         reloadPlayQueueManager(); | ||||
|     } | ||||
|  | ||||
|  | ||||
| @@ -2255,7 +2311,7 @@ public final class Player implements PlaybackListener, Listener { | ||||
|  | ||||
|     /** | ||||
|      * Get the video renderer index of the current playing stream. | ||||
|      * | ||||
|      * <p> | ||||
|      * This method returns the video renderer index of the current | ||||
|      * {@link MappingTrackSelector.MappedTrackInfo} or {@link #RENDERER_UNAVAILABLE} if the current | ||||
|      * {@link MappingTrackSelector.MappedTrackInfo} is null or if there is no video renderer index. | ||||
|   | ||||
| @@ -11,5 +11,6 @@ public interface PlayerEventListener { | ||||
|                           PlaybackParameters parameters); | ||||
|     void onProgressUpdate(int currentProgress, int duration, int bufferPercent); | ||||
|     void onMetadataUpdate(StreamInfo info, PlayQueue queue); | ||||
|     default void onAudioTrackUpdate() { } | ||||
|     void onServiceStopped(); | ||||
| } | ||||
|   | ||||
| @@ -7,6 +7,7 @@ import com.google.android.exoplayer2.MediaItem.RequestMetadata; | ||||
| import com.google.android.exoplayer2.MediaMetadata; | ||||
| import com.google.android.exoplayer2.Player; | ||||
|  | ||||
| import org.schabi.newpipe.extractor.stream.AudioStream; | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||
| import org.schabi.newpipe.extractor.stream.StreamType; | ||||
| import org.schabi.newpipe.extractor.stream.VideoStream; | ||||
| @@ -55,6 +56,11 @@ public interface MediaItemTag { | ||||
|         return Optional.empty(); | ||||
|     } | ||||
|  | ||||
|     @NonNull | ||||
|     default Optional<AudioTrack> getMaybeAudioTrack() { | ||||
|         return Optional.empty(); | ||||
|     } | ||||
|  | ||||
|     <T> Optional<T> getMaybeExtras(@NonNull Class<T> type); | ||||
|  | ||||
|     <T> MediaItemTag withExtras(@NonNull T extra); | ||||
| @@ -128,4 +134,37 @@ public interface MediaItemTag { | ||||
|                     ? null : sortedVideoStreams.get(selectedVideoStreamIndex); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     final class AudioTrack { | ||||
|         @NonNull | ||||
|         private final List<AudioStream> audioStreams; | ||||
|         private final int selectedAudioStreamIndex; | ||||
|  | ||||
|         private AudioTrack(@NonNull final List<AudioStream> audioStreams, | ||||
|                            final int selectedAudioStreamIndex) { | ||||
|             this.audioStreams = audioStreams; | ||||
|             this.selectedAudioStreamIndex = selectedAudioStreamIndex; | ||||
|         } | ||||
|  | ||||
|         static AudioTrack of(@NonNull final List<AudioStream> audioStreams, | ||||
|                              final int selectedAudioStreamIndex) { | ||||
|             return new AudioTrack(audioStreams, selectedAudioStreamIndex); | ||||
|         } | ||||
|  | ||||
|         @NonNull | ||||
|         public List<AudioStream> getAudioStreams() { | ||||
|             return audioStreams; | ||||
|         } | ||||
|  | ||||
|         public int getSelectedAudioStreamIndex() { | ||||
|             return selectedAudioStreamIndex; | ||||
|         } | ||||
|  | ||||
|         @Nullable | ||||
|         public AudioStream getSelectedAudioStream() { | ||||
|             return selectedAudioStreamIndex < 0 | ||||
|                     || selectedAudioStreamIndex >= audioStreams.size() | ||||
|                     ? null : audioStreams.get(selectedAudioStreamIndex); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -2,6 +2,7 @@ package org.schabi.newpipe.player.mediaitem; | ||||
|  | ||||
| import com.google.android.exoplayer2.MediaItem; | ||||
|  | ||||
| import org.schabi.newpipe.extractor.stream.AudioStream; | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||
| import org.schabi.newpipe.extractor.stream.StreamType; | ||||
| import org.schabi.newpipe.extractor.stream.VideoStream; | ||||
| @@ -25,25 +26,41 @@ public final class StreamInfoTag implements MediaItemTag { | ||||
|     @Nullable | ||||
|     private final MediaItemTag.Quality quality; | ||||
|     @Nullable | ||||
|     private final MediaItemTag.AudioTrack audioTrack; | ||||
|     @Nullable | ||||
|     private final Object extras; | ||||
|  | ||||
|     private StreamInfoTag(@NonNull final StreamInfo streamInfo, | ||||
|                           @Nullable final MediaItemTag.Quality quality, | ||||
|                           @Nullable final MediaItemTag.AudioTrack audioTrack, | ||||
|                           @Nullable final Object extras) { | ||||
|         this.streamInfo = streamInfo; | ||||
|         this.quality = quality; | ||||
|         this.audioTrack = audioTrack; | ||||
|         this.extras = extras; | ||||
|     } | ||||
|  | ||||
|     public static StreamInfoTag of(@NonNull final StreamInfo streamInfo, | ||||
|                                    @NonNull final List<VideoStream> sortedVideoStreams, | ||||
|                                    final int selectedVideoStreamIndex) { | ||||
|                                    final int selectedVideoStreamIndex, | ||||
|                                    @NonNull final List<AudioStream> audioStreams, | ||||
|                                    final int selectedAudioStreamIndex) { | ||||
|         final Quality quality = Quality.of(sortedVideoStreams, selectedVideoStreamIndex); | ||||
|         return new StreamInfoTag(streamInfo, quality, null); | ||||
|         final AudioTrack audioTrack = | ||||
|                 AudioTrack.of(audioStreams, selectedAudioStreamIndex); | ||||
|         return new StreamInfoTag(streamInfo, quality, audioTrack, null); | ||||
|     } | ||||
|  | ||||
|     public static StreamInfoTag of(@NonNull final StreamInfo streamInfo, | ||||
|                                    @NonNull final List<AudioStream> audioStreams, | ||||
|                                    final int selectedAudioStreamIndex) { | ||||
|         final AudioTrack audioTrack = | ||||
|                 AudioTrack.of(audioStreams, selectedAudioStreamIndex); | ||||
|         return new StreamInfoTag(streamInfo, null, audioTrack, null); | ||||
|     } | ||||
|  | ||||
|     public static StreamInfoTag of(@NonNull final StreamInfo streamInfo) { | ||||
|         return new StreamInfoTag(streamInfo, null, null); | ||||
|         return new StreamInfoTag(streamInfo, null, null, null); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
| @@ -103,6 +120,12 @@ public final class StreamInfoTag implements MediaItemTag { | ||||
|         return Optional.ofNullable(quality); | ||||
|     } | ||||
|  | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public Optional<AudioTrack> getMaybeAudioTrack() { | ||||
|         return Optional.ofNullable(audioTrack); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public <T> Optional<T> getMaybeExtras(@NonNull final Class<T> type) { | ||||
|         return Optional.ofNullable(extras).map(type::cast); | ||||
| @@ -110,6 +133,6 @@ public final class StreamInfoTag implements MediaItemTag { | ||||
|  | ||||
|     @Override | ||||
|     public StreamInfoTag withExtras(@NonNull final Object extra) { | ||||
|         return new StreamInfoTag(streamInfo, quality, extra); | ||||
|         return new StreamInfoTag(streamInfo, quality, audioTrack, extra); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| package org.schabi.newpipe.player.resolver; | ||||
|  | ||||
| import static org.schabi.newpipe.util.ListHelper.getFilteredAudioStreams; | ||||
| import static org.schabi.newpipe.util.ListHelper.getPlayableStreams; | ||||
|  | ||||
| import android.content.Context; | ||||
| @@ -28,6 +29,8 @@ public class AudioPlaybackResolver implements PlaybackResolver { | ||||
|     private final Context context; | ||||
|     @NonNull | ||||
|     private final PlayerDataSource dataSource; | ||||
|     @Nullable | ||||
|     private String audioTrack; | ||||
|  | ||||
|     public AudioPlaybackResolver(@NonNull final Context context, | ||||
|                                  @NonNull final PlayerDataSource dataSource) { | ||||
| @@ -35,6 +38,13 @@ public class AudioPlaybackResolver implements PlaybackResolver { | ||||
|         this.dataSource = dataSource; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get a media source providing audio. If a service has no separate {@link AudioStream}s we | ||||
|      * use a video stream as audio source to support audio background playback. | ||||
|      * | ||||
|      * @param info of the stream | ||||
|      * @return the audio source to use or null if none could be found | ||||
|      */ | ||||
|     @Override | ||||
|     @Nullable | ||||
|     public MediaSource resolve(@NonNull final StreamInfo info) { | ||||
| @@ -43,12 +53,27 @@ public class AudioPlaybackResolver implements PlaybackResolver { | ||||
|             return liveSource; | ||||
|         } | ||||
|  | ||||
|         final Stream stream = getAudioSource(info); | ||||
|         if (stream == null) { | ||||
|             return null; | ||||
|         } | ||||
|         final List<AudioStream> audioStreams = | ||||
|                 getFilteredAudioStreams(context, info.getAudioStreams()); | ||||
|         final Stream stream; | ||||
|         final MediaItemTag tag; | ||||
|  | ||||
|         final MediaItemTag tag = StreamInfoTag.of(info); | ||||
|         if (!audioStreams.isEmpty()) { | ||||
|             final int audioIndex = | ||||
|                     ListHelper.getAudioFormatIndex(context, audioStreams, audioTrack); | ||||
|             stream = getStreamForIndex(audioIndex, audioStreams); | ||||
|             tag = StreamInfoTag.of(info, audioStreams, audioIndex); | ||||
|         } else { | ||||
|             final List<VideoStream> videoStreams = | ||||
|                     getPlayableStreams(info.getVideoStreams(), info.getServiceId()); | ||||
|             if (!videoStreams.isEmpty()) { | ||||
|                 final int index = ListHelper.getDefaultResolutionIndex(context, videoStreams); | ||||
|                 stream = getStreamForIndex(index, videoStreams); | ||||
|                 tag = StreamInfoTag.of(info); | ||||
|             } else { | ||||
|                 return null; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         try { | ||||
|             return PlaybackResolver.buildMediaSource( | ||||
| @@ -59,31 +84,6 @@ public class AudioPlaybackResolver implements PlaybackResolver { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get a stream to be played as audio. If a service has no separate {@link AudioStream}s we | ||||
|      * use a video stream as audio source to support audio background playback. | ||||
|      * | ||||
|      * @param info of the stream | ||||
|      * @return the audio source to use or null if none could be found | ||||
|      */ | ||||
|     @Nullable | ||||
|     private Stream getAudioSource(@NonNull final StreamInfo info) { | ||||
|         final List<AudioStream> audioStreams = getPlayableStreams( | ||||
|                 info.getAudioStreams(), info.getServiceId()); | ||||
|         if (!audioStreams.isEmpty()) { | ||||
|             final int index = ListHelper.getDefaultAudioFormat(context, audioStreams); | ||||
|             return getStreamForIndex(index, audioStreams); | ||||
|         } else { | ||||
|             final List<VideoStream> videoStreams = getPlayableStreams( | ||||
|                     info.getVideoStreams(), info.getServiceId()); | ||||
|             if (!videoStreams.isEmpty()) { | ||||
|                 final int index = ListHelper.getDefaultResolutionIndex(context, videoStreams); | ||||
|                 return getStreamForIndex(index, videoStreams); | ||||
|             } | ||||
|         } | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|     @Nullable | ||||
|     Stream getStreamForIndex(final int index, @NonNull final List<? extends Stream> streams) { | ||||
|         if (index >= 0 && index < streams.size()) { | ||||
| @@ -91,4 +91,13 @@ public class AudioPlaybackResolver implements PlaybackResolver { | ||||
|         } | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|     @Nullable | ||||
|     public String getAudioTrack() { | ||||
|         return audioTrack; | ||||
|     } | ||||
|  | ||||
|     public void setAudioTrack(@Nullable final String audioLanguage) { | ||||
|         this.audioTrack = audioLanguage; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -156,6 +156,16 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> { | ||||
|             cacheKey.append(audioStream.getAverageBitrate()); | ||||
|         } | ||||
|  | ||||
|         if (audioStream.getAudioTrackId() != null) { | ||||
|             cacheKey.append(" "); | ||||
|             cacheKey.append(audioStream.getAudioTrackId()); | ||||
|         } | ||||
|  | ||||
|         if (audioStream.getAudioLocale() != null) { | ||||
|             cacheKey.append(" "); | ||||
|             cacheKey.append(audioStream.getAudioLocale().getISO3Language()); | ||||
|         } | ||||
|  | ||||
|         return cacheKey.toString(); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -28,6 +28,7 @@ import java.util.List; | ||||
| import java.util.Optional; | ||||
|  | ||||
| import static com.google.android.exoplayer2.C.TIME_UNSET; | ||||
| import static org.schabi.newpipe.util.ListHelper.getFilteredAudioStreams; | ||||
| import static org.schabi.newpipe.util.ListHelper.getUrlAndNonTorrentStreams; | ||||
| import static org.schabi.newpipe.util.ListHelper.getPlayableStreams; | ||||
|  | ||||
| @@ -44,6 +45,8 @@ public class VideoPlaybackResolver implements PlaybackResolver { | ||||
|  | ||||
|     @Nullable | ||||
|     private String playbackQuality; | ||||
|     @Nullable | ||||
|     private String audioTrack; | ||||
|  | ||||
|     public enum SourceType { | ||||
|         LIVE_STREAM, | ||||
| @@ -74,19 +77,29 @@ public class VideoPlaybackResolver implements PlaybackResolver { | ||||
|         final List<VideoStream> videoStreamsList = ListHelper.getSortedStreamVideosList(context, | ||||
|                 getPlayableStreams(info.getVideoStreams(), info.getServiceId()), | ||||
|                 getPlayableStreams(info.getVideoOnlyStreams(), info.getServiceId()), false, true); | ||||
|         final int index; | ||||
|         final List<AudioStream> audioStreamsList = | ||||
|                 getFilteredAudioStreams(context, info.getAudioStreams()); | ||||
|  | ||||
|         final int videoIndex; | ||||
|         if (videoStreamsList.isEmpty()) { | ||||
|             index = -1; | ||||
|             videoIndex = -1; | ||||
|         } else if (playbackQuality == null) { | ||||
|             index = qualityResolver.getDefaultResolutionIndex(videoStreamsList); | ||||
|             videoIndex = qualityResolver.getDefaultResolutionIndex(videoStreamsList); | ||||
|         } else { | ||||
|             index = qualityResolver.getOverrideResolutionIndex(videoStreamsList, | ||||
|             videoIndex = qualityResolver.getOverrideResolutionIndex(videoStreamsList, | ||||
|                     getPlaybackQuality()); | ||||
|         } | ||||
|         final MediaItemTag tag = StreamInfoTag.of(info, videoStreamsList, index); | ||||
|  | ||||
|         final int audioIndex = | ||||
|                 ListHelper.getAudioFormatIndex(context, audioStreamsList, audioTrack); | ||||
|         final MediaItemTag tag = | ||||
|                 StreamInfoTag.of(info, videoStreamsList, videoIndex, audioStreamsList, audioIndex); | ||||
|         @Nullable final VideoStream video = tag.getMaybeQuality() | ||||
|                 .map(MediaItemTag.Quality::getSelectedVideoStream) | ||||
|                 .orElse(null); | ||||
|         @Nullable final AudioStream audio = tag.getMaybeAudioTrack() | ||||
|                 .map(MediaItemTag.AudioTrack::getSelectedAudioStream) | ||||
|                 .orElse(null); | ||||
|  | ||||
|         if (video != null) { | ||||
|             try { | ||||
| @@ -99,15 +112,9 @@ public class VideoPlaybackResolver implements PlaybackResolver { | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Create optional audio stream source | ||||
|         final List<AudioStream> audioStreams = getPlayableStreams( | ||||
|                 info.getAudioStreams(), info.getServiceId()); | ||||
|         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())) { | ||||
|         if (audio != null && (video == null || video.isVideoOnly() || audioTrack != null)) { | ||||
|             try { | ||||
|                 final MediaSource audioSource = PlaybackResolver.buildMediaSource( | ||||
|                         dataSource, audio, info, PlaybackResolver.cacheKeyOf(info, audio), tag); | ||||
| @@ -180,6 +187,15 @@ public class VideoPlaybackResolver implements PlaybackResolver { | ||||
|         this.playbackQuality = playbackQuality; | ||||
|     } | ||||
|  | ||||
|     @Nullable | ||||
|     public String getAudioTrack() { | ||||
|         return audioTrack; | ||||
|     } | ||||
|  | ||||
|     public void setAudioTrack(@Nullable final String audioLanguage) { | ||||
|         this.audioTrack = audioLanguage; | ||||
|     } | ||||
|  | ||||
|     public interface QualityResolver { | ||||
|         int getDefaultResolutionIndex(List<VideoStream> sortedVideos); | ||||
|  | ||||
|   | ||||
| @@ -63,6 +63,7 @@ import org.schabi.newpipe.App; | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.databinding.PlayerBinding; | ||||
| 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.VideoStream; | ||||
| import org.schabi.newpipe.fragments.detail.VideoDetailFragment; | ||||
| @@ -78,6 +79,7 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItem; | ||||
| import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper; | ||||
| import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHolder; | ||||
| import org.schabi.newpipe.util.DeviceUtils; | ||||
| import org.schabi.newpipe.util.Localization; | ||||
| import org.schabi.newpipe.util.NavigationHelper; | ||||
| import org.schabi.newpipe.util.external_communication.KoreUtils; | ||||
| import org.schabi.newpipe.util.external_communication.ShareUtils; | ||||
| @@ -108,7 +110,8 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa | ||||
|  | ||||
|     protected PlayerBinding binding; | ||||
|     private final Handler controlsVisibilityHandler = new Handler(Looper.getMainLooper()); | ||||
|     @Nullable private SurfaceHolderCallback surfaceHolderCallback; | ||||
|     @Nullable | ||||
|     private SurfaceHolderCallback surfaceHolderCallback; | ||||
|     boolean surfaceIsSetup = false; | ||||
|  | ||||
|  | ||||
| @@ -117,11 +120,13 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     private static final int POPUP_MENU_ID_QUALITY = 69; | ||||
|     private static final int POPUP_MENU_ID_AUDIO_TRACK = 70; | ||||
|     private static final int POPUP_MENU_ID_PLAYBACK_SPEED = 79; | ||||
|     private static final int POPUP_MENU_ID_CAPTION = 89; | ||||
|  | ||||
|     protected boolean isSomePopupMenuVisible = false; | ||||
|     private PopupMenu qualityPopupMenu; | ||||
|     private PopupMenu audioTrackPopupMenu; | ||||
|     protected PopupMenu playbackSpeedPopupMenu; | ||||
|     private PopupMenu captionPopupMenu; | ||||
|  | ||||
| @@ -146,7 +151,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa | ||||
|     //region Constructor, setup, destroy | ||||
|  | ||||
|     protected VideoPlayerUi(@NonNull final Player player, | ||||
|                          @NonNull final PlayerBinding playerBinding) { | ||||
|                             @NonNull final PlayerBinding playerBinding) { | ||||
|         super(player); | ||||
|         binding = playerBinding; | ||||
|         setupFromView(); | ||||
| @@ -173,6 +178,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa | ||||
|                 R.style.DarkPopupMenu); | ||||
|  | ||||
|         qualityPopupMenu = new PopupMenu(themeWrapper, binding.qualityTextView); | ||||
|         audioTrackPopupMenu = new PopupMenu(themeWrapper, binding.audioTrackTextView); | ||||
|         playbackSpeedPopupMenu = new PopupMenu(context, binding.playbackSpeed); | ||||
|         captionPopupMenu = new PopupMenu(themeWrapper, binding.captionTextView); | ||||
|  | ||||
| @@ -190,6 +196,8 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa | ||||
|  | ||||
|     protected void initListeners() { | ||||
|         binding.qualityTextView.setOnClickListener(makeOnClickListener(this::onQualityClicked)); | ||||
|         binding.audioTrackTextView.setOnClickListener( | ||||
|                 makeOnClickListener(this::onAudioTracksClicked)); | ||||
|         binding.playbackSpeed.setOnClickListener(makeOnClickListener(this::onPlaybackSpeedClicked)); | ||||
|  | ||||
|         binding.playbackSeekBar.setOnSeekBarChangeListener(this); | ||||
| @@ -266,6 +274,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa | ||||
|  | ||||
|     protected void deinitListeners() { | ||||
|         binding.qualityTextView.setOnClickListener(null); | ||||
|         binding.audioTrackTextView.setOnClickListener(null); | ||||
|         binding.playbackSpeed.setOnClickListener(null); | ||||
|         binding.playbackSeekBar.setOnSeekBarChangeListener(null); | ||||
|         binding.captionTextView.setOnClickListener(null); | ||||
| @@ -419,6 +428,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa | ||||
|         binding.topControls.setPaddingRelative(controlsPad, playerTopPad, controlsPad, 0); | ||||
|         binding.bottomControls.setPaddingRelative(controlsPad, 0, controlsPad, 0); | ||||
|         binding.qualityTextView.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad); | ||||
|         binding.audioTrackTextView.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad); | ||||
|         binding.playbackSpeed.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad); | ||||
|         binding.playbackSpeed.setMinimumWidth(buttonsMinWidth); | ||||
|         binding.captionTextView.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad); | ||||
| @@ -524,6 +534,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa | ||||
|  | ||||
|     /** | ||||
|      * Sets the current duration into the corresponding elements. | ||||
|      * | ||||
|      * @param currentProgress the current progress, in milliseconds | ||||
|      */ | ||||
|     private void updatePlayBackElementsCurrentDuration(final int currentProgress) { | ||||
| @@ -536,6 +547,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa | ||||
|  | ||||
|     /** | ||||
|      * Sets the video duration time into all control components (e.g. seekbar). | ||||
|      * | ||||
|      * @param duration the video duration, in milliseconds | ||||
|      */ | ||||
|     private void setVideoDurationToControls(final int duration) { | ||||
| @@ -984,6 +996,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa | ||||
|     private void updateStreamRelatedViews() { | ||||
|         player.getCurrentStreamInfo().ifPresent(info -> { | ||||
|             binding.qualityTextView.setVisibility(View.GONE); | ||||
|             binding.audioTrackTextView.setVisibility(View.GONE); | ||||
|             binding.playbackSpeed.setVisibility(View.GONE); | ||||
|  | ||||
|             binding.playbackEndTime.setVisibility(View.GONE); | ||||
| @@ -1019,6 +1032,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa | ||||
|                     } | ||||
|  | ||||
|                     buildQualityMenu(); | ||||
|                     buildAudioTrackMenu(); | ||||
|  | ||||
|                     binding.qualityTextView.setVisibility(View.VISIBLE); | ||||
|                     binding.surfaceView.setVisibility(View.VISIBLE); | ||||
| @@ -1067,6 +1081,34 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa | ||||
|                 .ifPresent(s -> binding.qualityTextView.setText(s.getResolution())); | ||||
|     } | ||||
|  | ||||
|     private void buildAudioTrackMenu() { | ||||
|         if (audioTrackPopupMenu == null) { | ||||
|             return; | ||||
|         } | ||||
|         audioTrackPopupMenu.getMenu().removeGroup(POPUP_MENU_ID_AUDIO_TRACK); | ||||
|  | ||||
|         final List<AudioStream> availableStreams = Optional.ofNullable(player.getCurrentMetadata()) | ||||
|                 .flatMap(MediaItemTag::getMaybeAudioTrack) | ||||
|                 .map(MediaItemTag.AudioTrack::getAudioStreams) | ||||
|                 .orElse(null); | ||||
|         if (availableStreams == null || availableStreams.size() < 2) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         for (int i = 0; i < availableStreams.size(); i++) { | ||||
|             final AudioStream audioStream = availableStreams.get(i); | ||||
|             audioTrackPopupMenu.getMenu().add(POPUP_MENU_ID_AUDIO_TRACK, i, Menu.NONE, | ||||
|                     Localization.audioTrackName(context, audioStream)); | ||||
|         } | ||||
|  | ||||
|         player.getSelectedAudioStream() | ||||
|                 .ifPresent(s -> binding.audioTrackTextView.setText( | ||||
|                         Localization.audioTrackName(context, s))); | ||||
|         binding.audioTrackTextView.setVisibility(View.VISIBLE); | ||||
|         audioTrackPopupMenu.setOnMenuItemClickListener(this); | ||||
|         audioTrackPopupMenu.setOnDismissListener(this); | ||||
|     } | ||||
|  | ||||
|     private void buildPlaybackSpeedMenu() { | ||||
|         if (playbackSpeedPopupMenu == null) { | ||||
|             return; | ||||
| @@ -1175,6 +1217,11 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa | ||||
|                 .ifPresent(binding.qualityTextView::setText); | ||||
|     } | ||||
|  | ||||
|     private void onAudioTracksClicked() { | ||||
|         audioTrackPopupMenu.show(); | ||||
|         isSomePopupMenuVisible = true; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Called when an item of the quality selector or the playback speed selector is selected. | ||||
|      */ | ||||
| @@ -1187,26 +1234,10 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa | ||||
|         } | ||||
|  | ||||
|         if (menuItem.getGroupId() == POPUP_MENU_ID_QUALITY) { | ||||
|             final int menuItemIndex = menuItem.getItemId(); | ||||
|             @Nullable final MediaItemTag currentMetadata = player.getCurrentMetadata(); | ||||
|             if (currentMetadata == null || currentMetadata.getMaybeQuality().isEmpty()) { | ||||
|                 return true; | ||||
|             } | ||||
|  | ||||
|             final MediaItemTag.Quality quality = currentMetadata.getMaybeQuality().get(); | ||||
|             final List<VideoStream> availableStreams = quality.getSortedVideoStreams(); | ||||
|             final int selectedStreamIndex = quality.getSelectedVideoStreamIndex(); | ||||
|             if (selectedStreamIndex == menuItemIndex || availableStreams.size() <= menuItemIndex) { | ||||
|                 return true; | ||||
|             } | ||||
|  | ||||
|             player.saveStreamProgressState(); //TODO added, check if good | ||||
|             final String newResolution = availableStreams.get(menuItemIndex).getResolution(); | ||||
|             player.setRecovery(); | ||||
|             player.setPlaybackQuality(newResolution); | ||||
|             player.reloadPlayQueueManager(); | ||||
|  | ||||
|             binding.qualityTextView.setText(menuItem.getTitle()); | ||||
|             onQualityItemClick(menuItem); | ||||
|             return true; | ||||
|         } else if (menuItem.getGroupId() == POPUP_MENU_ID_AUDIO_TRACK) { | ||||
|             onAudioTrackItemClick(menuItem); | ||||
|             return true; | ||||
|         } else if (menuItem.getGroupId() == POPUP_MENU_ID_PLAYBACK_SPEED) { | ||||
|             final int speedIndex = menuItem.getItemId(); | ||||
| @@ -1219,6 +1250,47 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     private void onQualityItemClick(@NonNull final MenuItem menuItem) { | ||||
|         final int menuItemIndex = menuItem.getItemId(); | ||||
|         @Nullable final MediaItemTag currentMetadata = player.getCurrentMetadata(); | ||||
|         if (currentMetadata == null || currentMetadata.getMaybeQuality().isEmpty()) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         final MediaItemTag.Quality quality = currentMetadata.getMaybeQuality().get(); | ||||
|         final List<VideoStream> availableStreams = quality.getSortedVideoStreams(); | ||||
|         final int selectedStreamIndex = quality.getSelectedVideoStreamIndex(); | ||||
|         if (selectedStreamIndex == menuItemIndex || availableStreams.size() <= menuItemIndex) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         final String newResolution = availableStreams.get(menuItemIndex).getResolution(); | ||||
|         player.setPlaybackQuality(newResolution); | ||||
|  | ||||
|         binding.qualityTextView.setText(menuItem.getTitle()); | ||||
|     } | ||||
|  | ||||
|     private void onAudioTrackItemClick(@NonNull final MenuItem menuItem) { | ||||
|         final int menuItemIndex = menuItem.getItemId(); | ||||
|         @Nullable final MediaItemTag currentMetadata = player.getCurrentMetadata(); | ||||
|         if (currentMetadata == null || currentMetadata.getMaybeAudioTrack().isEmpty()) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         final MediaItemTag.AudioTrack audioTrack = | ||||
|                 currentMetadata.getMaybeAudioTrack().get(); | ||||
|         final List<AudioStream> availableStreams = audioTrack.getAudioStreams(); | ||||
|         final int selectedStreamIndex = audioTrack.getSelectedAudioStreamIndex(); | ||||
|         if (selectedStreamIndex == menuItemIndex || availableStreams.size() <= menuItemIndex) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         final String newAudioTrack = availableStreams.get(menuItemIndex).getAudioTrackId(); | ||||
|         player.setAudioTrack(newAudioTrack); | ||||
|  | ||||
|         binding.audioTrackTextView.setText(menuItem.getTitle()); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Called when some popup menu is dismissed. | ||||
|      */ | ||||
|   | ||||
| @@ -0,0 +1,94 @@ | ||||
| package org.schabi.newpipe.util; | ||||
|  | ||||
| import android.content.Context; | ||||
| import android.view.LayoutInflater; | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| import android.widget.BaseAdapter; | ||||
| import android.widget.ImageView; | ||||
| import android.widget.TextView; | ||||
|  | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
|  | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.extractor.stream.AudioStream; | ||||
| import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper; | ||||
|  | ||||
| import java.io.Serializable; | ||||
| import java.util.List; | ||||
| import java.util.stream.Collectors; | ||||
|  | ||||
| /** | ||||
|  * A list adapter for groups of {@link AudioStream}s (audio tracks). | ||||
|  */ | ||||
| public class AudioTrackAdapter extends BaseAdapter { | ||||
|     private final AudioTracksWrapper tracksWrapper; | ||||
|  | ||||
|     public AudioTrackAdapter(final AudioTracksWrapper tracksWrapper) { | ||||
|         this.tracksWrapper = tracksWrapper; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public int getCount() { | ||||
|         return tracksWrapper.size(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public List<AudioStream> getItem(final int position) { | ||||
|         return tracksWrapper.getTracksList().get(position).getStreamsList(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public long getItemId(final int position) { | ||||
|         return position; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public View getView(final int position, final View convertView, final ViewGroup parent) { | ||||
|         final var context = parent.getContext(); | ||||
|         final View view; | ||||
|         if (convertView == null) { | ||||
|             view = LayoutInflater.from(context).inflate( | ||||
|                     R.layout.stream_quality_item, parent, false); | ||||
|         } else { | ||||
|             view = convertView; | ||||
|         } | ||||
|  | ||||
|         final ImageView woSoundIconView = view.findViewById(R.id.wo_sound_icon); | ||||
|         final TextView formatNameView = view.findViewById(R.id.stream_format_name); | ||||
|         final TextView qualityView = view.findViewById(R.id.stream_quality); | ||||
|         final TextView sizeView = view.findViewById(R.id.stream_size); | ||||
|  | ||||
|         final List<AudioStream> streams = getItem(position); | ||||
|         final AudioStream stream = streams.get(0); | ||||
|  | ||||
|         woSoundIconView.setVisibility(View.GONE); | ||||
|         sizeView.setVisibility(View.VISIBLE); | ||||
|  | ||||
|         if (stream.getAudioTrackId() != null) { | ||||
|             formatNameView.setText(stream.getAudioTrackId()); | ||||
|         } | ||||
|         qualityView.setText(Localization.audioTrackName(context, stream)); | ||||
|  | ||||
|         return view; | ||||
|     } | ||||
|  | ||||
|     public static class AudioTracksWrapper implements Serializable { | ||||
|         private final List<StreamSizeWrapper<AudioStream>> tracksList; | ||||
|  | ||||
|         public AudioTracksWrapper(@NonNull final List<List<AudioStream>> groupedAudioStreams, | ||||
|                                   @Nullable final Context context) { | ||||
|             this.tracksList = groupedAudioStreams.stream().map(streams -> | ||||
|                     new StreamSizeWrapper<>(streams, context)).collect(Collectors.toList()); | ||||
|         } | ||||
|  | ||||
|         public List<StreamSizeWrapper<AudioStream>> getTracksList() { | ||||
|             return tracksList; | ||||
|         } | ||||
|  | ||||
|         public int size() { | ||||
|             return tracksList.size(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -15,6 +15,7 @@ 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.AudioTrackType; | ||||
| import org.schabi.newpipe.extractor.stream.DeliveryMethod; | ||||
| import org.schabi.newpipe.extractor.stream.Stream; | ||||
| import org.schabi.newpipe.extractor.stream.VideoStream; | ||||
| @@ -25,6 +26,7 @@ import java.util.Collections; | ||||
| import java.util.Comparator; | ||||
| import java.util.HashMap; | ||||
| import java.util.List; | ||||
| import java.util.Locale; | ||||
| import java.util.Objects; | ||||
| import java.util.Set; | ||||
| import java.util.function.Predicate; | ||||
| @@ -38,11 +40,17 @@ public final class ListHelper { | ||||
|     // Audio format in order of quality. 0=lowest quality, n=highest quality | ||||
|     private static final List<MediaFormat> AUDIO_FORMAT_QUALITY_RANKING = | ||||
|             List.of(MediaFormat.MP3, MediaFormat.WEBMA, MediaFormat.M4A); | ||||
|     // Audio format in order of efficiency. 0=most efficient, n=least efficient | ||||
|     // Audio format in order of efficiency. 0=least efficient, n=most efficient | ||||
|     private static final List<MediaFormat> AUDIO_FORMAT_EFFICIENCY_RANKING = | ||||
|             List.of(MediaFormat.WEBMA, MediaFormat.M4A, MediaFormat.MP3); | ||||
|             List.of(MediaFormat.MP3, MediaFormat.M4A, MediaFormat.WEBMA); | ||||
|     // Use a Set for better performance | ||||
|     private static final Set<String> HIGH_RESOLUTION_LIST = Set.of("1440p", "2160p"); | ||||
|     // Audio track types in order of priotity. 0=lowest, n=highest | ||||
|     private static final List<AudioTrackType> AUDIO_TRACK_TYPE_RANKING = | ||||
|             List.of(AudioTrackType.DESCRIPTIVE, AudioTrackType.DUBBED, AudioTrackType.ORIGINAL); | ||||
|     // Audio track types in order of priotity when descriptive audio is preferred. | ||||
|     private static final List<AudioTrackType> AUDIO_TRACK_TYPE_RANKING_DESCRIPTIVE = | ||||
|             List.of(AudioTrackType.ORIGINAL, AudioTrackType.DUBBED, AudioTrackType.DESCRIPTIVE); | ||||
|  | ||||
|     /** | ||||
|      * List of supported YouTube Itag ids. | ||||
| @@ -62,10 +70,10 @@ public final class ListHelper { | ||||
|     private ListHelper() { } | ||||
|  | ||||
|     /** | ||||
|      * @see #getDefaultResolutionIndex(String, String, MediaFormat, List) | ||||
|      * @param context      Android app context | ||||
|      * @param videoStreams list of the video streams to check | ||||
|      * @return index of the video stream with the default index | ||||
|      * @see #getDefaultResolutionIndex(String, String, MediaFormat, List) | ||||
|      */ | ||||
|     public static int getDefaultResolutionIndex(final Context context, | ||||
|                                                 final List<VideoStream> videoStreams) { | ||||
| @@ -75,11 +83,11 @@ public final class ListHelper { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @see #getDefaultResolutionIndex(String, String, MediaFormat, List) | ||||
|      * @param context           Android app context | ||||
|      * @param videoStreams      list of the video streams to check | ||||
|      * @param defaultResolution the default resolution to look for | ||||
|      * @return index of the video stream with the default index | ||||
|      * @see #getDefaultResolutionIndex(String, String, MediaFormat, List) | ||||
|      */ | ||||
|     public static int getResolutionIndex(final Context context, | ||||
|                                          final List<VideoStream> videoStreams, | ||||
| @@ -88,10 +96,10 @@ public final class ListHelper { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @see #getDefaultResolutionIndex(String, String, MediaFormat, List) | ||||
|      * @param context           Android app context | ||||
|      * @param videoStreams      list of the video streams to check | ||||
|      * @param context      Android app context | ||||
|      * @param videoStreams list of the video streams to check | ||||
|      * @return index of the video stream with the default index | ||||
|      * @see #getDefaultResolutionIndex(String, String, MediaFormat, List) | ||||
|      */ | ||||
|     public static int getPopupDefaultResolutionIndex(final Context context, | ||||
|                                                      final List<VideoStream> videoStreams) { | ||||
| @@ -101,11 +109,11 @@ public final class ListHelper { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @see #getDefaultResolutionIndex(String, String, MediaFormat, List) | ||||
|      * @param context           Android app context | ||||
|      * @param videoStreams      list of the video streams to check | ||||
|      * @param defaultResolution the default resolution to look for | ||||
|      * @return index of the video stream with the default index | ||||
|      * @see #getDefaultResolutionIndex(String, String, MediaFormat, List) | ||||
|      */ | ||||
|     public static int getPopupResolutionIndex(final Context context, | ||||
|                                               final List<VideoStream> videoStreams, | ||||
| @@ -115,16 +123,36 @@ public final class ListHelper { | ||||
|  | ||||
|     public static int getDefaultAudioFormat(final Context context, | ||||
|                                             final List<AudioStream> audioStreams) { | ||||
|         final MediaFormat defaultFormat = getDefaultFormat(context, | ||||
|                 R.string.default_audio_format_key, R.string.default_audio_format_value); | ||||
|         return getAudioIndexByHighestRank(audioStreams, | ||||
|                 getAudioTrackComparator(context).thenComparing(getAudioFormatComparator(context))); | ||||
|     } | ||||
|  | ||||
|         // If the user has chosen to limit resolution to conserve mobile data | ||||
|         // usage then we should also limit our audio usage. | ||||
|         if (isLimitingDataUsage(context)) { | ||||
|             return getMostCompactAudioIndex(defaultFormat, audioStreams); | ||||
|         } else { | ||||
|             return getHighestQualityAudioIndex(defaultFormat, audioStreams); | ||||
|     public static int getDefaultAudioTrackGroup(final Context context, | ||||
|                                                 final List<List<AudioStream>> groupedAudioStreams) { | ||||
|         if (groupedAudioStreams == null || groupedAudioStreams.isEmpty()) { | ||||
|             return -1; | ||||
|         } | ||||
|  | ||||
|         final Comparator<AudioStream> cmp = getAudioTrackComparator(context); | ||||
|         final List<AudioStream> highestRanked = groupedAudioStreams.stream() | ||||
|                 .max((o1, o2) -> cmp.compare(o1.get(0), o2.get(0))) | ||||
|                 .orElse(null); | ||||
|         return groupedAudioStreams.indexOf(highestRanked); | ||||
|     } | ||||
|  | ||||
|     public static int getAudioFormatIndex(final Context context, | ||||
|                                           final List<AudioStream> audioStreams, | ||||
|                                           @Nullable final String trackId) { | ||||
|         if (trackId != null) { | ||||
|             for (int i = 0; i < audioStreams.size(); i++) { | ||||
|                 final AudioStream s = audioStreams.get(i); | ||||
|                 if (s.getAudioTrackId() != null | ||||
|                         && s.getAudioTrackId().equals(trackId)) { | ||||
|                     return i; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         return getDefaultAudioFormat(context, audioStreams); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -211,6 +239,90 @@ public final class ListHelper { | ||||
|                 videoOnlyStreams, ascendingOrder, preferVideoOnlyStreams); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Filter the list of audio streams and return a list with the preferred stream for | ||||
|      * each audio track. Streams are sorted with the preferred language in the first position. | ||||
|      * | ||||
|      * @param context      the context to search for the track to give preference | ||||
|      * @param audioStreams the list of audio streams | ||||
|      * @return the sorted, filtered list | ||||
|      */ | ||||
|     public static List<AudioStream> getFilteredAudioStreams( | ||||
|             @NonNull final Context context, | ||||
|             @Nullable final List<AudioStream> audioStreams) { | ||||
|         if (audioStreams == null) { | ||||
|             return Collections.emptyList(); | ||||
|         } | ||||
|  | ||||
|         final HashMap<String, AudioStream> collectedStreams = new HashMap<>(); | ||||
|  | ||||
|         final Comparator<AudioStream> cmp = getAudioFormatComparator(context); | ||||
|  | ||||
|         for (final AudioStream stream : audioStreams) { | ||||
|             if (stream.getDeliveryMethod() == DeliveryMethod.TORRENT) { | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             final String trackId = Objects.toString(stream.getAudioTrackId(), ""); | ||||
|  | ||||
|             final AudioStream presentStream = collectedStreams.get(trackId); | ||||
|             if (presentStream == null || cmp.compare(stream, presentStream) > 0) { | ||||
|                 collectedStreams.put(trackId, stream); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Filter unknown audio tracks if there are multiple tracks | ||||
|         if (collectedStreams.size() > 1) { | ||||
|             collectedStreams.remove(""); | ||||
|         } | ||||
|  | ||||
|         // Sort collected streams by name | ||||
|         return collectedStreams.values().stream().sorted(getAudioTrackNameComparator(context)) | ||||
|                 .collect(Collectors.toList()); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Group the list of audioStreams by their track ID and sort the resulting list by track name. | ||||
|      * | ||||
|      * @param context      app context to get track names for sorting | ||||
|      * @param audioStreams list of audio streams | ||||
|      * @return list of audio streams lists representing individual tracks | ||||
|      */ | ||||
|     public static List<List<AudioStream>> getGroupedAudioStreams( | ||||
|             @NonNull final Context context, | ||||
|             @Nullable final List<AudioStream> audioStreams) { | ||||
|         if (audioStreams == null) { | ||||
|             return Collections.emptyList(); | ||||
|         } | ||||
|  | ||||
|         final HashMap<String, List<AudioStream>> collectedStreams = new HashMap<>(); | ||||
|  | ||||
|         for (final AudioStream stream : audioStreams) { | ||||
|             final String trackId = Objects.toString(stream.getAudioTrackId(), ""); | ||||
|             if (collectedStreams.containsKey(trackId)) { | ||||
|                 collectedStreams.get(trackId).add(stream); | ||||
|             } else { | ||||
|                 final List<AudioStream> list = new ArrayList<>(); | ||||
|                 list.add(stream); | ||||
|                 collectedStreams.put(trackId, list); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Filter unknown audio tracks if there are multiple tracks | ||||
|         if (collectedStreams.size() > 1) { | ||||
|             collectedStreams.remove(""); | ||||
|         } | ||||
|  | ||||
|         // Sort tracks alphabetically, sort track streams by quality | ||||
|         final Comparator<AudioStream> nameCmp = getAudioTrackNameComparator(context); | ||||
|         final Comparator<AudioStream> formatCmp = getAudioFormatComparator(context); | ||||
|  | ||||
|         return collectedStreams.values().stream() | ||||
|                 .sorted((o1, o2) -> nameCmp.compare(o1.get(0), o2.get(0))) | ||||
|                 .map(streams -> streams.stream().sorted(formatCmp).collect(Collectors.toList())) | ||||
|                 .collect(Collectors.toList()); | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Utils | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
| @@ -325,8 +437,8 @@ public final class ListHelper { | ||||
|                 // Filter out higher resolutions (or not if high resolutions should always be shown) | ||||
|                 .filter(stream -> showHigherResolutions | ||||
|                         || !HIGH_RESOLUTION_LIST.contains(stream.getResolution() | ||||
|                                 // Replace any frame rate with nothing | ||||
|                                 .replaceAll("p\\d+$", "p"))) | ||||
|                         // Replace any frame rate with nothing | ||||
|                         .replaceAll("p\\d+$", "p"))) | ||||
|                 .collect(Collectors.toList()); | ||||
|  | ||||
|         final HashMap<String, VideoStream> hashMap = new HashMap<>(); | ||||
| @@ -376,72 +488,22 @@ public final class ListHelper { | ||||
|         return videoStreams; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get the audio from the list with the highest quality. | ||||
|      * Format will be ignored if it yields no results. | ||||
|      * | ||||
|      * @param format       The target format type or null if it doesn't matter | ||||
|      * @param audioStreams List of audio streams | ||||
|      * @return Index of audio stream that produces the most compact results or -1 if not found | ||||
|      */ | ||||
|     static int getHighestQualityAudioIndex(@Nullable final MediaFormat format, | ||||
|                                            @Nullable final List<AudioStream> audioStreams) { | ||||
|         return getAudioIndexByHighestRank(format, audioStreams, | ||||
|                 // Compares descending (last = highest rank) | ||||
|                 getAudioStreamComparator(AUDIO_FORMAT_QUALITY_RANKING)); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get the audio from the list with the lowest bitrate and most efficient format. | ||||
|      * Format will be ignored if it yields no results. | ||||
|      * | ||||
|      * @param format       The target format type or null if it doesn't matter | ||||
|      * @param audioStreams List of audio streams | ||||
|      * @return Index of audio stream that produces the most compact results or -1 if not found | ||||
|      */ | ||||
|     static int getMostCompactAudioIndex(@Nullable final MediaFormat format, | ||||
|                                         @Nullable final List<AudioStream> audioStreams) { | ||||
|         return getAudioIndexByHighestRank(format, audioStreams, | ||||
|                 // The "reversed()" is important -> Compares ascending (first = highest rank) | ||||
|                 getAudioStreamComparator(AUDIO_FORMAT_EFFICIENCY_RANKING).reversed()); | ||||
|     } | ||||
|  | ||||
|     private static Comparator<AudioStream> getAudioStreamComparator( | ||||
|             final List<MediaFormat> formatRanking) { | ||||
|         return Comparator.nullsLast(Comparator.comparingInt(AudioStream::getAverageBitrate)) | ||||
|                 .thenComparingInt(stream -> formatRanking.indexOf(stream.getFormat())); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get the audio-stream from the list with the highest rank, depending on the comparator. | ||||
|      * Format will be ignored if it yields no results. | ||||
|      * | ||||
|      * @param targetedFormat The target format type or null if it doesn't matter | ||||
|      * @param audioStreams   List of audio streams | ||||
|      * @param comparator     The comparator used for determining the max/best/highest ranked value | ||||
|      * @param audioStreams List of audio streams | ||||
|      * @param comparator   The comparator used for determining the max/best/highest ranked value | ||||
|      * @return Index of audio stream that produces the highest ranked result or -1 if not found | ||||
|      */ | ||||
|     private static int getAudioIndexByHighestRank(@Nullable final MediaFormat targetedFormat, | ||||
|                                                   @Nullable final List<AudioStream> audioStreams, | ||||
|                                                   final Comparator<AudioStream> comparator) { | ||||
|     static int getAudioIndexByHighestRank(@Nullable final List<AudioStream> audioStreams, | ||||
|                                           final Comparator<AudioStream> comparator) { | ||||
|         if (audioStreams == null || audioStreams.isEmpty()) { | ||||
|             return -1; | ||||
|         } | ||||
|  | ||||
|         final AudioStream highestRankedAudioStream = audioStreams.stream() | ||||
|                 .filter(audioStream -> targetedFormat == null | ||||
|                         || audioStream.getFormat() == targetedFormat) | ||||
|                 .max(comparator) | ||||
|                 .orElse(null); | ||||
|  | ||||
|         if (highestRankedAudioStream == null) { | ||||
|             // Fallback: Ignore targetedFormat if not null | ||||
|             if (targetedFormat != null) { | ||||
|                 return getAudioIndexByHighestRank(null, audioStreams, comparator); | ||||
|             } | ||||
|             // targetedFormat is already null -> return -1 | ||||
|             return -1; | ||||
|         } | ||||
|                 .max(comparator).orElse(null); | ||||
|  | ||||
|         return audioStreams.indexOf(highestRankedAudioStream); | ||||
|     } | ||||
| @@ -629,4 +691,149 @@ public final class ListHelper { | ||||
|  | ||||
|         return manager.isActiveNetworkMetered(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get a {@link Comparator} to compare {@link AudioStream}s by their format and bitrate. | ||||
|      * | ||||
|      * <p>The prefered stream will be ordered last.</p> | ||||
|      * | ||||
|      * @param context app context | ||||
|      * @return Comparator | ||||
|      */ | ||||
|     private static Comparator<AudioStream> getAudioFormatComparator( | ||||
|             final @NonNull Context context) { | ||||
|         final MediaFormat defaultFormat = getDefaultFormat(context, | ||||
|                 R.string.default_audio_format_key, R.string.default_audio_format_value); | ||||
|         return getAudioFormatComparator(defaultFormat, isLimitingDataUsage(context)); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get a {@link Comparator} to compare {@link AudioStream}s by their format and bitrate. | ||||
|      * | ||||
|      * <p>The prefered stream will be ordered last.</p> | ||||
|      * | ||||
|      * @param defaultFormat  the default format to look for | ||||
|      * @param limitDataUsage choose low bitrate audio stream | ||||
|      * @return Comparator | ||||
|      */ | ||||
|     static Comparator<AudioStream> getAudioFormatComparator( | ||||
|             @Nullable final MediaFormat defaultFormat, final boolean limitDataUsage) { | ||||
|         final List<MediaFormat> formatRanking = limitDataUsage | ||||
|                 ? AUDIO_FORMAT_EFFICIENCY_RANKING : AUDIO_FORMAT_QUALITY_RANKING; | ||||
|  | ||||
|         Comparator<AudioStream> bitrateComparator = | ||||
|                 Comparator.comparingInt(AudioStream::getAverageBitrate); | ||||
|         if (limitDataUsage) { | ||||
|             bitrateComparator = bitrateComparator.reversed(); | ||||
|         } | ||||
|  | ||||
|         return Comparator.comparing(AudioStream::getFormat, (o1, o2) -> { | ||||
|             if (defaultFormat != null) { | ||||
|                 return Boolean.compare(o1 == defaultFormat, o2 == defaultFormat); | ||||
|             } | ||||
|             return 0; | ||||
|         }).thenComparing(bitrateComparator).thenComparingInt( | ||||
|                 stream -> formatRanking.indexOf(stream.getFormat())); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get a {@link Comparator} to compare {@link AudioStream}s by their tracks. | ||||
|      * | ||||
|      * <p>Tracks will be compared this order:</p> | ||||
|      * <ol> | ||||
|      * <li>If {@code preferOriginalAudio}: use original audio</li> | ||||
|      * <li>Language matches {@code preferredLanguage}</li> | ||||
|      * <li> | ||||
|      *     Track type ranks highest in this order: | ||||
|      *     <i>Original</i> > <i>Dubbed</i> > <i>Descriptive</i> | ||||
|      *     <p>If {@code preferDescriptiveAudio}: | ||||
|      *     <i>Descriptive</i> > <i>Dubbed</i> > <i>Original</i></p> | ||||
|      * </li> | ||||
|      * <li>Language is English</li> | ||||
|      * </ol> | ||||
|      * | ||||
|      * <p>The prefered track will be ordered last.</p> | ||||
|      * | ||||
|      * @param context App context | ||||
|      * @return Comparator | ||||
|      */ | ||||
|     private static Comparator<AudioStream> getAudioTrackComparator( | ||||
|             @NonNull final Context context) { | ||||
|         final SharedPreferences preferences = | ||||
|                 PreferenceManager.getDefaultSharedPreferences(context); | ||||
|         final Locale preferredLanguage = Localization.getPreferredLocale(context); | ||||
|         final boolean preferOriginalAudio = | ||||
|                 preferences.getBoolean(context.getString(R.string.prefer_original_audio_key), | ||||
|                         false); | ||||
|         final boolean preferDescriptiveAudio = | ||||
|                 preferences.getBoolean(context.getString(R.string.prefer_descriptive_audio_key), | ||||
|                         false); | ||||
|  | ||||
|         return getAudioTrackComparator(preferredLanguage, preferOriginalAudio, | ||||
|                 preferDescriptiveAudio); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get a {@link Comparator} to compare {@link AudioStream}s by their tracks. | ||||
|      * | ||||
|      * <p>Tracks will be compared this order:</p> | ||||
|      * <ol> | ||||
|      * <li>If {@code preferOriginalAudio}: use original audio</li> | ||||
|      * <li>Language matches {@code preferredLanguage}</li> | ||||
|      * <li> | ||||
|      *     Track type ranks highest in this order: | ||||
|      *     <i>Original</i> > <i>Dubbed</i> > <i>Descriptive</i> | ||||
|      *     <p>If {@code preferDescriptiveAudio}: | ||||
|      *     <i>Descriptive</i> > <i>Dubbed</i> > <i>Original</i></p> | ||||
|      * </li> | ||||
|      * <li>Language is English</li> | ||||
|      * </ol> | ||||
|      * | ||||
|      * <p>The prefered track will be ordered last.</p> | ||||
|      * | ||||
|      * @param preferredLanguage      Preferred audio stream language | ||||
|      * @param preferOriginalAudio    Get the original audio track regardless of its language | ||||
|      * @param preferDescriptiveAudio Prefer the descriptive audio track if available | ||||
|      * @return Comparator | ||||
|      */ | ||||
|     static Comparator<AudioStream> getAudioTrackComparator( | ||||
|             final Locale preferredLanguage, | ||||
|             final boolean preferOriginalAudio, | ||||
|             final boolean preferDescriptiveAudio) { | ||||
|         final String langCode = preferredLanguage.getISO3Language(); | ||||
|         final List<AudioTrackType> trackTypeRanking = preferDescriptiveAudio | ||||
|                 ? AUDIO_TRACK_TYPE_RANKING_DESCRIPTIVE : AUDIO_TRACK_TYPE_RANKING; | ||||
|  | ||||
|         return Comparator.comparing(AudioStream::getAudioTrackType, (o1, o2) -> { | ||||
|                     if (preferOriginalAudio) { | ||||
|                         return Boolean.compare( | ||||
|                                 o1 == AudioTrackType.ORIGINAL, o2 == AudioTrackType.ORIGINAL); | ||||
|                     } | ||||
|                     return 0; | ||||
|                 }).thenComparing(AudioStream::getAudioLocale, | ||||
|                         Comparator.nullsFirst(Comparator.comparing( | ||||
|                                 locale -> locale.getISO3Language().equals(langCode)))) | ||||
|                 .thenComparing(AudioStream::getAudioTrackType, | ||||
|                         Comparator.nullsFirst(Comparator.comparingInt(trackTypeRanking::indexOf))) | ||||
|                 .thenComparing(AudioStream::getAudioLocale, | ||||
|                         Comparator.nullsFirst(Comparator.comparing( | ||||
|                                 locale -> locale.getISO3Language().equals( | ||||
|                                         Locale.ENGLISH.getISO3Language())))); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get a {@link Comparator} to compare {@link AudioStream}s by their languages and track types | ||||
|      * for alphabetical sorting. | ||||
|      * | ||||
|      * @param context app context for localization | ||||
|      * @return Comparator | ||||
|      */ | ||||
|     private static Comparator<AudioStream> getAudioTrackNameComparator( | ||||
|             @NonNull final Context context) { | ||||
|         final Locale appLoc = Localization.getAppLocale(context); | ||||
|  | ||||
|         return Comparator.comparing(AudioStream::getAudioLocale, Comparator.nullsLast( | ||||
|                         Comparator.comparing(locale -> locale.getDisplayName(appLoc)))) | ||||
|                 .thenComparing(AudioStream::getAudioTrackType); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -11,6 +11,7 @@ import android.text.TextUtils; | ||||
| import android.util.DisplayMetrics; | ||||
|  | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.annotation.PluralsRes; | ||||
| import androidx.annotation.StringRes; | ||||
| import androidx.core.math.MathUtils; | ||||
| @@ -21,6 +22,8 @@ import org.ocpsoft.prettytime.units.Decade; | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.extractor.ListExtractor; | ||||
| import org.schabi.newpipe.extractor.localization.ContentCountry; | ||||
| import org.schabi.newpipe.extractor.stream.AudioStream; | ||||
| import org.schabi.newpipe.extractor.stream.AudioTrackType; | ||||
|  | ||||
| import java.math.BigDecimal; | ||||
| import java.math.RoundingMode; | ||||
| @@ -261,6 +264,52 @@ public final class Localization { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get the localized name of an audio track. | ||||
|      * | ||||
|      * <p>Examples of results returned by this method:</p> | ||||
|      * <ul> | ||||
|      *     <li>English (original)</li> | ||||
|      *     <li>English (descriptive)</li> | ||||
|      *     <li>Spanish (dubbed)</li> | ||||
|      * </ul> | ||||
|      * | ||||
|      * @param context the context used to get the app language | ||||
|      * @param track   an {@link AudioStream} of the track | ||||
|      * @return the localized name of the audio track | ||||
|      */ | ||||
|     public static String audioTrackName(final Context context, final AudioStream track) { | ||||
|         final String name; | ||||
|         if (track.getAudioLocale() != null) { | ||||
|             name = track.getAudioLocale().getDisplayLanguage(getAppLocale(context)); | ||||
|         } else if (track.getAudioTrackName() != null) { | ||||
|             name = track.getAudioTrackName(); | ||||
|         } else { | ||||
|             name = context.getString(R.string.unknown_audio_track); | ||||
|         } | ||||
|  | ||||
|         if (track.getAudioTrackType() != null) { | ||||
|             final String trackType = audioTrackType(context, track.getAudioTrackType()); | ||||
|             if (trackType != null) { | ||||
|                 return context.getString(R.string.audio_track_name, name, trackType); | ||||
|             } | ||||
|         } | ||||
|         return name; | ||||
|     } | ||||
|  | ||||
|     @Nullable | ||||
|     private static String audioTrackType(final Context context, final AudioTrackType trackType) { | ||||
|         switch (trackType) { | ||||
|             case ORIGINAL: | ||||
|                 return context.getString(R.string.audio_track_type_original); | ||||
|             case DUBBED: | ||||
|                 return context.getString(R.string.audio_track_type_dubbed); | ||||
|             case DESCRIPTIVE: | ||||
|                 return context.getString(R.string.audio_track_type_descriptive); | ||||
|         } | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Pretty Time | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|   | ||||
| @@ -224,6 +224,8 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA | ||||
|     public static class StreamSizeWrapper<T extends Stream> implements Serializable { | ||||
|         private static final StreamSizeWrapper<Stream> EMPTY = | ||||
|                 new StreamSizeWrapper<>(Collections.emptyList(), null); | ||||
|         private static final int SIZE_UNSET = -2; | ||||
|  | ||||
|         private final List<T> streamsList; | ||||
|         private final long[] streamSizes; | ||||
|         private final String unknownSize; | ||||
| @@ -235,7 +237,7 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA | ||||
|             this.unknownSize = context == null | ||||
|                     ? "--.-" : context.getString(R.string.unknown_content); | ||||
|  | ||||
|             Arrays.fill(streamSizes, -2); | ||||
|             resetSizes(); | ||||
|         } | ||||
|  | ||||
|         /** | ||||
| @@ -251,7 +253,7 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA | ||||
|             final Callable<Boolean> fetchAndSet = () -> { | ||||
|                 boolean hasChanged = false; | ||||
|                 for (final X stream : streamsWrapper.getStreamsList()) { | ||||
|                     if (streamsWrapper.getSizeInBytes(stream) > -2) { | ||||
|                     if (streamsWrapper.getSizeInBytes(stream) > SIZE_UNSET) { | ||||
|                         continue; | ||||
|                     } | ||||
|  | ||||
| @@ -269,6 +271,10 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA | ||||
|                     .onErrorReturnItem(true); | ||||
|         } | ||||
|  | ||||
|         public void resetSizes() { | ||||
|             Arrays.fill(streamSizes, SIZE_UNSET); | ||||
|         } | ||||
|  | ||||
|         public static <X extends Stream> StreamSizeWrapper<X> empty() { | ||||
|             //noinspection unchecked | ||||
|             return (StreamSizeWrapper<X>) EMPTY; | ||||
|   | ||||
| @@ -71,11 +71,45 @@ | ||||
|         android:minWidth="150dp" | ||||
|         tools:listitem="@layout/stream_quality_item" /> | ||||
|  | ||||
|     <Spinner | ||||
|         android:id="@+id/audio_track_spinner" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_below="@+id/quality_spinner" | ||||
|         android:layout_marginLeft="20dp" | ||||
|         android:layout_marginRight="20dp" | ||||
|         android:layout_marginBottom="12dp" | ||||
|         android:minWidth="150dp" | ||||
|         tools:visibility="gone" /> | ||||
|  | ||||
|     <Spinner | ||||
|         android:id="@+id/audio_stream_spinner" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_below="@+id/audio_track_spinner" | ||||
|         android:layout_marginLeft="20dp" | ||||
|         android:layout_marginRight="20dp" | ||||
|         android:layout_marginBottom="12dp" | ||||
|         android:minWidth="150dp" | ||||
|         tools:visibility="gone" /> | ||||
|  | ||||
|     <org.schabi.newpipe.views.NewPipeTextView | ||||
|         android:id="@+id/audio_track_present_in_video_text" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_below="@+id/audio_stream_spinner" | ||||
|         android:layout_marginLeft="24dp" | ||||
|         android:layout_marginRight="24dp" | ||||
|         android:layout_marginBottom="12dp" | ||||
|         android:gravity="center" | ||||
|         android:text="@string/audio_track_present_in_video" | ||||
|         android:textSize="12sp" /> | ||||
|  | ||||
|     <org.schabi.newpipe.views.NewPipeTextView | ||||
|         android:id="@+id/threads_text_view" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_below="@+id/quality_spinner" | ||||
|         android:layout_below="@+id/audio_track_present_in_video_text" | ||||
|         android:layout_marginLeft="24dp" | ||||
|         android:layout_marginRight="24dp" | ||||
|         android:layout_marginBottom="6dp" | ||||
|   | ||||
| @@ -157,6 +157,22 @@ | ||||
|                             tools:text="The Video Artist  LONG very LONG very Long" /> | ||||
|                     </LinearLayout> | ||||
|  | ||||
|                     <org.schabi.newpipe.views.NewPipeTextView | ||||
|                         android:id="@+id/audioTrackTextView" | ||||
|                         android:layout_width="wrap_content" | ||||
|                         android:layout_height="35dp" | ||||
|                         android:layout_marginEnd="8dp" | ||||
|                         android:background="?attr/selectableItemBackground" | ||||
|                         android:gravity="center" | ||||
|                         android:minWidth="0dp" | ||||
|                         android:padding="@dimen/player_main_buttons_padding" | ||||
|                         android:textColor="@android:color/white" | ||||
|                         android:textStyle="bold" | ||||
|                         android:visibility="gone" | ||||
|                         tools:ignore="HardcodedText,RtlHardcoded" | ||||
|                         tools:visibility="visible" | ||||
|                         tools:text="English (Original)" /> | ||||
|  | ||||
|                     <org.schabi.newpipe.views.NewPipeTextView | ||||
|                         android:id="@+id/qualityTextView" | ||||
|                         android:layout_width="wrap_content" | ||||
|   | ||||
| @@ -18,6 +18,14 @@ | ||||
|         android:visible="true" | ||||
|         app:showAsAction="ifRoom" /> | ||||
|  | ||||
|     <item | ||||
|         android:id="@+id/action_audio_track" | ||||
|         android:tooltipText="@string/audio_track" | ||||
|         android:visible="false" | ||||
|         app:showAsAction="ifRoom"> | ||||
|         <menu /> | ||||
|     </item> | ||||
|  | ||||
|     <item | ||||
|         android:id="@+id/action_mute" | ||||
|         android:icon="@drawable/ic_volume_off" | ||||
|   | ||||
| @@ -219,6 +219,8 @@ | ||||
|         <item>@string/none_control_key</item> | ||||
|     </string-array> | ||||
|  | ||||
|     <string name="prefer_original_audio_key">prefer_original_audio</string> | ||||
|     <string name="prefer_descriptive_audio_key">prefer_descriptive_audio</string> | ||||
|     <string name="last_resize_mode">last_resize_mode</string> | ||||
|  | ||||
|     <!-- DEBUG ONLY --> | ||||
|   | ||||
| @@ -94,6 +94,10 @@ | ||||
|     <string name="show_description_summary">Turn off to hide video description and additional information</string> | ||||
|     <string name="show_meta_info_title">Show meta info</string> | ||||
|     <string name="show_meta_info_summary">Turn off to hide meta info boxes with additional information about the stream creator, stream content or a search request</string> | ||||
|     <string name="prefer_original_audio_title">Prefer original audio</string> | ||||
|     <string name="prefer_original_audio_summary">Select the original audio track regardless of the language</string> | ||||
|     <string name="prefer_descriptive_audio_title">Prefer descriptive audio</string> | ||||
|     <string name="prefer_descriptive_audio_summary">Select an audio track with descriptions for visually impaired people if available</string> | ||||
|     <string name="thumbnail_cache_wipe_complete_notice">Image cache wiped</string> | ||||
|     <string name="metadata_cache_wipe_title">Wipe cached metadata</string> | ||||
|     <string name="metadata_cache_wipe_summary">Remove all cached webpage data</string> | ||||
| @@ -414,6 +418,8 @@ | ||||
|     <string name="play_queue_remove">Remove</string> | ||||
|     <string name="play_queue_stream_detail">Details</string> | ||||
|     <string name="play_queue_audio_settings">Audio Settings</string> | ||||
|     <string name="play_queue_audio_track">Audio: %s</string> | ||||
|     <string name="audio_track">Audio track</string> | ||||
|     <string name="hold_to_append">Hold to enqueue</string> | ||||
|     <string name="show_channel_details">Show channel details</string> | ||||
|     <string name="enqueue_stream">Enqueue</string> | ||||
| @@ -761,12 +767,15 @@ | ||||
|     <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="audio_track_present_in_video">An audio track should be already present in this stream</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="select_audio_track_external_players">Select audio track for external players</string> | ||||
|     <string name="unknown_format">Unknown format</string> | ||||
|     <string name="unknown_quality">Unknown quality</string> | ||||
|     <string name="unknown_audio_track">Unknown</string> | ||||
|     <string name="feed_toggle_show_future_items">Show future items</string> | ||||
|     <string name="feed_toggle_hide_future_items">Hide future items</string> | ||||
|     <string name="feed_show_watched">Fully watched</string> | ||||
| @@ -779,4 +788,8 @@ | ||||
|     <string name="use_exoplayer_decoder_fallback_summary">Enable this option if you have decoder initialization issues, which falls back to lower-priority decoders if primary decoders initialization fail. This may result in poor playback performance than when using primary decoders</string> | ||||
|     <string name="always_use_exoplayer_set_output_surface_workaround_title">Always use ExoPlayer\'s video output surface setting workaround</string> | ||||
|     <string name="always_use_exoplayer_set_output_surface_workaround_summary">This workaround releases and re-instantiates video codecs when a surface change occurs, instead of setting the surface to the codec directly. Already used by ExoPlayer on some devices with this issue, this setting has only an effect on Android 6 and higher\n\nEnabling this option may prevent playback errors when switching the current video player or switching to fullscreen</string> | ||||
|     <string name="audio_track_name">%s %s</string> | ||||
|     <string name="audio_track_type_original">original</string> | ||||
|     <string name="audio_track_type_dubbed">dubbed</string> | ||||
|     <string name="audio_track_type_descriptive">descriptive</string> | ||||
| </resources> | ||||
| @@ -61,6 +61,22 @@ | ||||
|         app:iconSpaceReserved="false" | ||||
|         app:useSimpleSummaryProvider="true" /> | ||||
|  | ||||
|     <SwitchPreferenceCompat | ||||
|         android:defaultValue="false" | ||||
|         android:key="@string/prefer_original_audio_key" | ||||
|         android:summary="@string/prefer_original_audio_summary" | ||||
|         android:title="@string/prefer_original_audio_title" | ||||
|         app:singleLineTitle="false" | ||||
|         app:iconSpaceReserved="false"/> | ||||
|  | ||||
|     <SwitchPreferenceCompat | ||||
|         android:defaultValue="false" | ||||
|         android:key="@string/prefer_descriptive_audio_key" | ||||
|         android:summary="@string/prefer_descriptive_audio_summary" | ||||
|         android:title="@string/prefer_descriptive_audio_title" | ||||
|         app:singleLineTitle="false" | ||||
|         app:iconSpaceReserved="false"/> | ||||
|  | ||||
|     <PreferenceScreen | ||||
|         android:fragment="org.schabi.newpipe.settings.ExoPlayerSettingsFragment" | ||||
|         android:key="@string/exoplayer_settings_key" | ||||
|   | ||||
| @@ -3,10 +3,13 @@ package org.schabi.newpipe.util; | ||||
| import org.junit.Test; | ||||
| import org.schabi.newpipe.extractor.MediaFormat; | ||||
| import org.schabi.newpipe.extractor.stream.AudioStream; | ||||
| import org.schabi.newpipe.extractor.stream.AudioTrackType; | ||||
| import org.schabi.newpipe.extractor.stream.VideoStream; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.Comparator; | ||||
| import java.util.List; | ||||
| import java.util.Locale; | ||||
|  | ||||
| import static org.junit.Assert.assertEquals; | ||||
| import static org.junit.Assert.assertFalse; | ||||
| @@ -29,6 +32,15 @@ public class ListHelperTest { | ||||
|             generateAudioStream("mp3-192", MediaFormat.MP3, 192), | ||||
|             generateAudioStream("webma-320", MediaFormat.WEBMA, 320)); | ||||
|  | ||||
|     private static final List<AudioStream> AUDIO_TRACKS_TEST_LIST = List.of( | ||||
|             generateAudioTrack("en.or", "en.or", Locale.ENGLISH, AudioTrackType.ORIGINAL), | ||||
|             generateAudioTrack("en.du", "en.du", Locale.ENGLISH, AudioTrackType.DUBBED), | ||||
|             generateAudioTrack("en.ds", "en.ds", Locale.ENGLISH, AudioTrackType.DESCRIPTIVE), | ||||
|             generateAudioTrack("unknown", null, null, null), | ||||
|             generateAudioTrack("de.du", "de.du", Locale.GERMAN, AudioTrackType.DUBBED), | ||||
|             generateAudioTrack("de.ds", "de.ds", Locale.GERMAN, AudioTrackType.DESCRIPTIVE) | ||||
|     ); | ||||
|  | ||||
|     private static final List<VideoStream> VIDEO_STREAMS_TEST_LIST = List.of( | ||||
|             generateVideoStream("mpeg_4-720", MediaFormat.MPEG_4, "720p", false), | ||||
|             generateVideoStream("v3gpp-240", MediaFormat.v3GPP, "240p", false), | ||||
| @@ -199,24 +211,29 @@ public class ListHelperTest { | ||||
|  | ||||
|     @Test | ||||
|     public void getHighestQualityAudioFormatTest() { | ||||
|         AudioStream stream = AUDIO_STREAMS_TEST_LIST.get(ListHelper.getHighestQualityAudioIndex( | ||||
|                 MediaFormat.M4A, AUDIO_STREAMS_TEST_LIST)); | ||||
|         Comparator<AudioStream> cmp = ListHelper.getAudioFormatComparator(MediaFormat.M4A, false); | ||||
|         AudioStream stream = AUDIO_STREAMS_TEST_LIST.get(ListHelper.getAudioIndexByHighestRank( | ||||
|                         AUDIO_STREAMS_TEST_LIST, cmp)); | ||||
|         assertEquals(320, stream.getAverageBitrate()); | ||||
|         assertEquals(MediaFormat.M4A, stream.getFormat()); | ||||
|  | ||||
|         stream = AUDIO_STREAMS_TEST_LIST.get(ListHelper.getHighestQualityAudioIndex( | ||||
|                 MediaFormat.WEBMA, AUDIO_STREAMS_TEST_LIST)); | ||||
|         cmp = ListHelper.getAudioFormatComparator(MediaFormat.WEBMA, false); | ||||
|         stream = AUDIO_STREAMS_TEST_LIST.get(ListHelper.getAudioIndexByHighestRank( | ||||
|                 AUDIO_STREAMS_TEST_LIST, cmp)); | ||||
|         assertEquals(320, stream.getAverageBitrate()); | ||||
|         assertEquals(MediaFormat.WEBMA, stream.getFormat()); | ||||
|  | ||||
|         stream = AUDIO_STREAMS_TEST_LIST.get(ListHelper.getHighestQualityAudioIndex( | ||||
|                 MediaFormat.MP3, AUDIO_STREAMS_TEST_LIST)); | ||||
|         cmp = ListHelper.getAudioFormatComparator(MediaFormat.MP3, false); | ||||
|         stream = AUDIO_STREAMS_TEST_LIST.get(ListHelper.getAudioIndexByHighestRank( | ||||
|                 AUDIO_STREAMS_TEST_LIST, cmp)); | ||||
|         assertEquals(192, stream.getAverageBitrate()); | ||||
|         assertEquals(MediaFormat.MP3, stream.getFormat()); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void getHighestQualityAudioFormatPreferredAbsent() { | ||||
|         final Comparator<AudioStream> cmp = | ||||
|                 ListHelper.getAudioFormatComparator(MediaFormat.MP3, false); | ||||
|  | ||||
|         ////////////////////////////////////////// | ||||
|         // Doesn't contain the preferred format // | ||||
| @@ -227,8 +244,7 @@ public class ListHelperTest { | ||||
|                 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( | ||||
|                 MediaFormat.MP3, testList)); | ||||
|         AudioStream stream = testList.get(ListHelper.getAudioIndexByHighestRank(testList, cmp)); | ||||
|         assertEquals(192, stream.getAverageBitrate()); | ||||
|         assertEquals(MediaFormat.WEBMA, stream.getFormat()); | ||||
|  | ||||
| @@ -246,44 +262,51 @@ public class ListHelperTest { | ||||
|                 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)); | ||||
|         stream = | ||||
|                 testList.get(ListHelper.getAudioIndexByHighestRank(testList, cmp)); | ||||
|         assertEquals(192, stream.getAverageBitrate()); | ||||
|         assertEquals(MediaFormat.M4A, stream.getFormat()); | ||||
|  | ||||
|         // Adding a new format and bitrate. Adding another stream will have no impact since | ||||
|         // it's not a preferred format. | ||||
|         testList.add(generateAudioStream("webma-192-5", MediaFormat.WEBMA, 192)); | ||||
|         stream = testList.get(ListHelper.getHighestQualityAudioIndex(MediaFormat.MP3, testList)); | ||||
|         stream = | ||||
|                 testList.get(ListHelper.getAudioIndexByHighestRank(testList, cmp)); | ||||
|         assertEquals(192, stream.getAverageBitrate()); | ||||
|         assertEquals(MediaFormat.M4A, stream.getFormat()); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void getHighestQualityAudioNull() { | ||||
|         assertEquals(-1, ListHelper.getHighestQualityAudioIndex(null, null)); | ||||
|         assertEquals(-1, ListHelper.getHighestQualityAudioIndex(null, new ArrayList<>())); | ||||
|         final Comparator<AudioStream> cmp = ListHelper.getAudioFormatComparator(null, false); | ||||
|         assertEquals(-1, ListHelper.getAudioIndexByHighestRank(null, cmp)); | ||||
|         assertEquals(-1, ListHelper.getAudioIndexByHighestRank(new ArrayList<>(), cmp)); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void getLowestQualityAudioFormatTest() { | ||||
|         AudioStream stream = AUDIO_STREAMS_TEST_LIST.get(ListHelper.getMostCompactAudioIndex( | ||||
|                 MediaFormat.M4A, AUDIO_STREAMS_TEST_LIST)); | ||||
|         Comparator<AudioStream> cmp = ListHelper.getAudioFormatComparator(MediaFormat.M4A, true); | ||||
|         AudioStream stream = AUDIO_STREAMS_TEST_LIST.get(ListHelper.getAudioIndexByHighestRank( | ||||
|                 AUDIO_STREAMS_TEST_LIST, cmp)); | ||||
|         assertEquals(128, stream.getAverageBitrate()); | ||||
|         assertEquals(MediaFormat.M4A, stream.getFormat()); | ||||
|  | ||||
|         stream = AUDIO_STREAMS_TEST_LIST.get(ListHelper.getMostCompactAudioIndex( | ||||
|                 MediaFormat.WEBMA, AUDIO_STREAMS_TEST_LIST)); | ||||
|         cmp = ListHelper.getAudioFormatComparator(MediaFormat.WEBMA, true); | ||||
|         stream = AUDIO_STREAMS_TEST_LIST.get(ListHelper.getAudioIndexByHighestRank( | ||||
|                 AUDIO_STREAMS_TEST_LIST, cmp)); | ||||
|         assertEquals(64, stream.getAverageBitrate()); | ||||
|         assertEquals(MediaFormat.WEBMA, stream.getFormat()); | ||||
|  | ||||
|         stream = AUDIO_STREAMS_TEST_LIST.get(ListHelper.getMostCompactAudioIndex( | ||||
|                 MediaFormat.MP3, AUDIO_STREAMS_TEST_LIST)); | ||||
|         cmp = ListHelper.getAudioFormatComparator(MediaFormat.MP3, true); | ||||
|         stream = AUDIO_STREAMS_TEST_LIST.get(ListHelper.getAudioIndexByHighestRank( | ||||
|                 AUDIO_STREAMS_TEST_LIST, cmp)); | ||||
|         assertEquals(64, stream.getAverageBitrate()); | ||||
|         assertEquals(MediaFormat.MP3, stream.getFormat()); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void getLowestQualityAudioFormatPreferredAbsent() { | ||||
|         Comparator<AudioStream> cmp = ListHelper.getAudioFormatComparator(MediaFormat.MP3, true); | ||||
|  | ||||
|         ////////////////////////////////////////// | ||||
|         // Doesn't contain the preferred format // | ||||
| @@ -294,14 +317,13 @@ public class ListHelperTest { | ||||
|                 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( | ||||
|                 MediaFormat.MP3, testList)); | ||||
|         AudioStream stream = testList.get(ListHelper.getAudioIndexByHighestRank(testList, cmp)); | ||||
|         assertEquals(128, stream.getAverageBitrate()); | ||||
|         assertEquals(MediaFormat.M4A, stream.getFormat()); | ||||
|  | ||||
|         // WEBMA is more compact than M4A | ||||
|         testList.add(generateAudioStream("webma-192-2", MediaFormat.WEBMA, 128)); | ||||
|         stream = testList.get(ListHelper.getMostCompactAudioIndex(MediaFormat.MP3, testList)); | ||||
|         stream = testList.get(ListHelper.getAudioIndexByHighestRank(testList, cmp)); | ||||
|         assertEquals(128, stream.getAverageBitrate()); | ||||
|         assertEquals(MediaFormat.WEBMA, stream.getFormat()); | ||||
|  | ||||
| @@ -318,20 +340,58 @@ public class ListHelperTest { | ||||
|                 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)); | ||||
|         stream = testList.get( | ||||
|                 ListHelper.getAudioIndexByHighestRank(testList, cmp)); | ||||
|         assertEquals(192, stream.getAverageBitrate()); | ||||
|         assertEquals(MediaFormat.WEBMA, stream.getFormat()); | ||||
|  | ||||
|         // Should be same as above | ||||
|         stream = testList.get(ListHelper.getMostCompactAudioIndex(null, testList)); | ||||
|         cmp = ListHelper.getAudioFormatComparator(null, true); | ||||
|         stream = testList.get( | ||||
|                 ListHelper.getAudioIndexByHighestRank(testList, cmp)); | ||||
|         assertEquals(192, stream.getAverageBitrate()); | ||||
|         assertEquals(MediaFormat.WEBMA, stream.getFormat()); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void getLowestQualityAudioNull() { | ||||
|         assertEquals(-1, ListHelper.getMostCompactAudioIndex(null, null)); | ||||
|         assertEquals(-1, ListHelper.getMostCompactAudioIndex(null, new ArrayList<>())); | ||||
|         final Comparator<AudioStream> cmp = ListHelper.getAudioFormatComparator(null, false); | ||||
|         assertEquals(-1, ListHelper.getAudioIndexByHighestRank(null, cmp)); | ||||
|         assertEquals(-1, ListHelper.getAudioIndexByHighestRank(new ArrayList<>(), cmp)); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void getAudioTrack() { | ||||
|         // English language | ||||
|         Comparator<AudioStream> cmp = | ||||
|                 ListHelper.getAudioTrackComparator(Locale.ENGLISH, false, false); | ||||
|         AudioStream stream = AUDIO_TRACKS_TEST_LIST.get(ListHelper.getAudioIndexByHighestRank( | ||||
|                 AUDIO_TRACKS_TEST_LIST, cmp)); | ||||
|         assertEquals("en.or", stream.getId()); | ||||
|  | ||||
|         // German language | ||||
|         cmp = ListHelper.getAudioTrackComparator(Locale.GERMAN, false, false); | ||||
|         stream = AUDIO_TRACKS_TEST_LIST.get(ListHelper.getAudioIndexByHighestRank( | ||||
|                 AUDIO_TRACKS_TEST_LIST, cmp)); | ||||
|         assertEquals("de.du", stream.getId()); | ||||
|  | ||||
|         // German language, but prefer original | ||||
|         cmp = ListHelper.getAudioTrackComparator(Locale.GERMAN, true, false); | ||||
|         stream = AUDIO_TRACKS_TEST_LIST.get(ListHelper.getAudioIndexByHighestRank( | ||||
|                 AUDIO_TRACKS_TEST_LIST, cmp)); | ||||
|         assertEquals("en.or", stream.getId()); | ||||
|  | ||||
|         // Prefer descriptive audio | ||||
|         cmp = ListHelper.getAudioTrackComparator(Locale.ENGLISH, false, true); | ||||
|         stream = AUDIO_TRACKS_TEST_LIST.get(ListHelper.getAudioIndexByHighestRank( | ||||
|                 AUDIO_TRACKS_TEST_LIST, cmp)); | ||||
|         assertEquals("en.ds", stream.getId()); | ||||
|  | ||||
|         // Japanese language, fall back to original | ||||
|         cmp = ListHelper.getAudioTrackComparator(Locale.JAPANESE, true, false); | ||||
|         stream = AUDIO_TRACKS_TEST_LIST.get(ListHelper.getAudioIndexByHighestRank( | ||||
|                 AUDIO_TRACKS_TEST_LIST, cmp)); | ||||
|         assertEquals("en.or", stream.getId()); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
| @@ -390,6 +450,22 @@ public class ListHelperTest { | ||||
|                 .build(); | ||||
|     } | ||||
|  | ||||
|     private static AudioStream generateAudioTrack( | ||||
|             @NonNull final String id, | ||||
|             @Nullable final String trackId, | ||||
|             @Nullable final Locale locale, | ||||
|             @Nullable final AudioTrackType trackType) { | ||||
|         return new AudioStream.Builder() | ||||
|                 .setId(id) | ||||
|                 .setContent("", true) | ||||
|                 .setMediaFormat(MediaFormat.M4A) | ||||
|                 .setAverageBitrate(128) | ||||
|                 .setAudioTrackId(trackId) | ||||
|                 .setAudioLocale(locale) | ||||
|                 .setAudioTrackType(trackType) | ||||
|                 .build(); | ||||
|     } | ||||
|  | ||||
|     @NonNull | ||||
|     private static VideoStream generateVideoStream(@NonNull final String id, | ||||
|                                                    @Nullable final MediaFormat mediaFormat, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Stypox
					Stypox