From 2e771cd65ab1eff2cb090351a4083d1d129fe53a Mon Sep 17 00:00:00 2001
From: GGAutomaton <32899400+GGAutomaton@users.noreply.github.com>
Date: Mon, 4 Apr 2022 23:58:39 +0800
Subject: [PATCH 001/240] Fix crash when rotating device on unsupported
channels
---
.../fragments/list/channel/ChannelFragment.java | 13 ++++++++++++-
1 file changed, 12 insertions(+), 1 deletion(-)
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java
index 869503b5bed..6ad81132002 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java
@@ -77,6 +77,8 @@ public class ChannelFragment extends BaseListInfoFragment
Date: Sat, 1 Jan 2022 19:02:03 +0100
Subject: [PATCH 002/240] Major refactoring of PlaybackParameterDialog
* Removed/Renamed methods
* Use ``IcePick``
* Better structuring
* Keep skipSilence when rotating the device (PlayQueueActivity only)
---
.../helper/PlaybackParameterDialog.java | 738 ++++++------------
1 file changed, 230 insertions(+), 508 deletions(-)
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java
index 1a55c21c342..b72b7ea7b0f 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java
@@ -9,10 +9,10 @@
import android.util.Log;
import android.view.View;
import android.widget.CheckBox;
-import android.widget.RelativeLayout;
import android.widget.SeekBar;
import android.widget.TextView;
+import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
@@ -20,104 +20,88 @@
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R;
-import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener;
import org.schabi.newpipe.util.SliderStrategy;
+import java.util.Objects;
+import java.util.function.DoubleConsumer;
+import java.util.function.DoubleFunction;
+
+import icepick.Icepick;
+import icepick.State;
+
public class PlaybackParameterDialog extends DialogFragment {
+ private static final String TAG = "PlaybackParameterDialog";
+
// Minimum allowable range in ExoPlayer
private static final double MINIMUM_PLAYBACK_VALUE = 0.10f;
private static final double MAXIMUM_PLAYBACK_VALUE = 3.00f;
- private static final char STEP_UP_SIGN = '+';
- private static final char STEP_DOWN_SIGN = '-';
-
- private static final double STEP_ONE_PERCENT_VALUE = 0.01f;
- private static final double STEP_FIVE_PERCENT_VALUE = 0.05f;
- private static final double STEP_TEN_PERCENT_VALUE = 0.10f;
- private static final double STEP_TWENTY_FIVE_PERCENT_VALUE = 0.25f;
- private static final double STEP_ONE_HUNDRED_PERCENT_VALUE = 1.00f;
+ private static final double STEP_1_PERCENT_VALUE = 0.01f;
+ private static final double STEP_5_PERCENT_VALUE = 0.05f;
+ private static final double STEP_10_PERCENT_VALUE = 0.10f;
+ private static final double STEP_25_PERCENT_VALUE = 0.25f;
+ private static final double STEP_100_PERCENT_VALUE = 1.00f;
private static final double DEFAULT_TEMPO = 1.00f;
private static final double DEFAULT_PITCH = 1.00f;
- private static final int DEFAULT_SEMITONES = 0;
- private static final double DEFAULT_STEP = STEP_TWENTY_FIVE_PERCENT_VALUE;
+ private static final double DEFAULT_STEP = STEP_25_PERCENT_VALUE;
private static final boolean DEFAULT_SKIP_SILENCE = false;
- @NonNull
- private static final String TAG = "PlaybackParameterDialog";
- @NonNull
- private static final String INITIAL_TEMPO_KEY = "initial_tempo_key";
- @NonNull
- private static final String INITIAL_PITCH_KEY = "initial_pitch_key";
-
- @NonNull
- private static final String TEMPO_KEY = "tempo_key";
- @NonNull
- private static final String PITCH_KEY = "pitch_key";
- @NonNull
- private static final String STEP_SIZE_KEY = "step_size_key";
-
- @NonNull
- private final SliderStrategy strategy = new SliderStrategy.Quadratic(
- MINIMUM_PLAYBACK_VALUE, MAXIMUM_PLAYBACK_VALUE,
- /*centerAt=*/1.00f, /*sliderGranularity=*/10000);
+ private static final SliderStrategy QUADRATIC_STRATEGY = new SliderStrategy.Quadratic(
+ MINIMUM_PLAYBACK_VALUE,
+ MAXIMUM_PLAYBACK_VALUE,
+ 1.00f,
+ 10_000);
@Nullable
private Callback callback;
- private double initialTempo = DEFAULT_TEMPO;
- private double initialPitch = DEFAULT_PITCH;
- private int initialSemitones = DEFAULT_SEMITONES;
- private boolean initialSkipSilence = DEFAULT_SKIP_SILENCE;
- private double tempo = DEFAULT_TEMPO;
- private double pitch = DEFAULT_PITCH;
- private int semitones = DEFAULT_SEMITONES;
+ @State
+ double initialTempo = DEFAULT_TEMPO;
+ @State
+ double initialPitch = DEFAULT_PITCH;
+ @State
+ boolean initialSkipSilence = DEFAULT_SKIP_SILENCE;
+
+ @State
+ double tempo = DEFAULT_TEMPO;
+ @State
+ double pitch = DEFAULT_PITCH;
+ @State
+ double stepSize = DEFAULT_STEP;
+ @State
+ boolean skipSilence = DEFAULT_SKIP_SILENCE;
- @Nullable
private SeekBar tempoSlider;
- @Nullable
private TextView tempoCurrentText;
- @Nullable
private TextView tempoStepDownText;
- @Nullable
private TextView tempoStepUpText;
- @Nullable
+
private SeekBar pitchSlider;
- @Nullable
private TextView pitchCurrentText;
- @Nullable
private TextView pitchStepDownText;
- @Nullable
private TextView pitchStepUpText;
- @Nullable
- private SeekBar semitoneSlider;
- @Nullable
- private TextView semitoneCurrentText;
- @Nullable
- private TextView semitoneStepDownText;
- @Nullable
- private TextView semitoneStepUpText;
- @Nullable
+
private CheckBox unhookingCheckbox;
- @Nullable
private CheckBox skipSilenceCheckbox;
- @Nullable
- private CheckBox adjustBySemitonesCheckbox;
- public static PlaybackParameterDialog newInstance(final double playbackTempo,
- final double playbackPitch,
- final boolean playbackSkipSilence,
- final Callback callback) {
+ public static PlaybackParameterDialog newInstance(
+ final double playbackTempo,
+ final double playbackPitch,
+ final boolean playbackSkipSilence,
+ final Callback callback
+ ) {
final PlaybackParameterDialog dialog = new PlaybackParameterDialog();
dialog.callback = callback;
+
dialog.initialTempo = playbackTempo;
dialog.initialPitch = playbackPitch;
+ dialog.initialSkipSilence = playbackSkipSilence;
- dialog.tempo = playbackTempo;
- dialog.pitch = playbackPitch;
- dialog.semitones = dialog.percentToSemitones(playbackPitch);
+ dialog.tempo = dialog.initialTempo;
+ dialog.pitch = dialog.initialPitch;
+ dialog.skipSilence = dialog.initialSkipSilence;
- dialog.initialSkipSilence = playbackSkipSilence;
return dialog;
}
@@ -126,7 +110,7 @@ public static PlaybackParameterDialog newInstance(final double playbackTempo,
//////////////////////////////////////////////////////////////////////////*/
@Override
- public void onAttach(@NonNull final Context context) {
+ public void onAttach(final Context context) {
super.onAttach(context);
if (context instanceof Callback) {
callback = (Callback) context;
@@ -136,28 +120,9 @@ public void onAttach(@NonNull final Context context) {
}
@Override
- public void onCreate(@Nullable final Bundle savedInstanceState) {
- assureCorrectAppLanguage(getContext());
- super.onCreate(savedInstanceState);
- if (savedInstanceState != null) {
- initialTempo = savedInstanceState.getDouble(INITIAL_TEMPO_KEY, DEFAULT_TEMPO);
- initialPitch = savedInstanceState.getDouble(INITIAL_PITCH_KEY, DEFAULT_PITCH);
- initialSemitones = percentToSemitones(initialPitch);
-
- tempo = savedInstanceState.getDouble(TEMPO_KEY, DEFAULT_TEMPO);
- pitch = savedInstanceState.getDouble(PITCH_KEY, DEFAULT_PITCH);
- semitones = percentToSemitones(pitch);
- }
- }
-
- @Override
- public void onSaveInstanceState(@NonNull final Bundle outState) {
+ public void onSaveInstanceState(final Bundle outState) {
super.onSaveInstanceState(outState);
- outState.putDouble(INITIAL_TEMPO_KEY, initialTempo);
- outState.putDouble(INITIAL_PITCH_KEY, initialPitch);
-
- outState.putDouble(TEMPO_KEY, getCurrentTempo());
- outState.putDouble(PITCH_KEY, getCurrentPitch());
+ Icepick.saveInstanceState(this, outState);
}
/*//////////////////////////////////////////////////////////////////////////
@@ -168,20 +133,28 @@ public void onSaveInstanceState(@NonNull final Bundle outState) {
@Override
public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) {
assureCorrectAppLanguage(getContext());
+ Icepick.restoreInstanceState(this, savedInstanceState);
+
final View view = View.inflate(getContext(), R.layout.dialog_playback_parameter, null);
- setupControlViews(view);
+ initUI(view);
+ initUIData();
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireActivity())
.setView(view)
.setCancelable(true)
- .setNegativeButton(R.string.cancel, (dialogInterface, i) ->
- setPlaybackParameters(initialTempo, initialPitch,
- initialSemitones, initialSkipSilence))
- .setNeutralButton(R.string.playback_reset, (dialogInterface, i) ->
- setPlaybackParameters(DEFAULT_TEMPO, DEFAULT_PITCH,
- DEFAULT_SEMITONES, DEFAULT_SKIP_SILENCE))
- .setPositiveButton(R.string.ok, (dialogInterface, i) ->
- setCurrentPlaybackParameters());
+ .setNegativeButton(R.string.cancel, (dialogInterface, i) -> {
+ setAndUpdateTempo(initialTempo);
+ setAndUpdatePitch(initialPitch);
+ setAndUpdateSkipSilence(initialSkipSilence);
+ updateCallback();
+ })
+ .setNeutralButton(R.string.playback_reset, (dialogInterface, i) -> {
+ setAndUpdateTempo(DEFAULT_TEMPO);
+ setAndUpdatePitch(DEFAULT_PITCH);
+ setAndUpdateSkipSilence(DEFAULT_SKIP_SILENCE);
+ updateCallback();
+ })
+ .setPositiveButton(R.string.ok, (dialogInterface, i) -> updateCallback());
return dialogBuilder.create();
}
@@ -190,353 +163,171 @@ public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) {
// Control Views
//////////////////////////////////////////////////////////////////////////*/
- private void setupControlViews(@NonNull final View rootView) {
- setupHookingControl(rootView);
- setupSkipSilenceControl(rootView);
- setupAdjustBySemitonesControl(rootView);
-
- setupTempoControl(rootView);
- setupPitchControl(rootView);
- setupSemitoneControl(rootView);
-
- togglePitchSliderType(rootView);
-
- setupStepSizeSelector(rootView);
- }
-
- private void togglePitchSliderType(@NonNull final View rootView) {
- final RelativeLayout pitchControl = rootView.findViewById(R.id.pitchControl);
- final RelativeLayout semitoneControl = rootView.findViewById(R.id.semitoneControl);
-
- final View separatorStepSizeSelector =
- rootView.findViewById(R.id.separatorStepSizeSelector);
- final RelativeLayout.LayoutParams params =
- (RelativeLayout.LayoutParams) separatorStepSizeSelector.getLayoutParams();
- if (pitchControl != null && semitoneControl != null && unhookingCheckbox != null) {
- if (getCurrentAdjustBySemitones()) {
- // replaces pitchControl slider with semitoneControl slider
- pitchControl.setVisibility(View.GONE);
- semitoneControl.setVisibility(View.VISIBLE);
- params.addRule(RelativeLayout.BELOW, R.id.semitoneControl);
-
- // forces unhook for semitones
- unhookingCheckbox.setChecked(true);
- unhookingCheckbox.setEnabled(false);
-
- setupTempoStepSizeSelector(rootView);
- } else {
- semitoneControl.setVisibility(View.GONE);
- pitchControl.setVisibility(View.VISIBLE);
- params.addRule(RelativeLayout.BELOW, R.id.pitchControl);
-
- // (re)enables hooking selection
- unhookingCheckbox.setEnabled(true);
- setupCombinedStepSizeSelector(rootView);
- }
- }
- }
-
- private void setupTempoControl(@NonNull final View rootView) {
- tempoSlider = rootView.findViewById(R.id.tempoSeekbar);
- final TextView tempoMinimumText = rootView.findViewById(R.id.tempoMinimumText);
- final TextView tempoMaximumText = rootView.findViewById(R.id.tempoMaximumText);
- tempoCurrentText = rootView.findViewById(R.id.tempoCurrentText);
- tempoStepUpText = rootView.findViewById(R.id.tempoStepUp);
- tempoStepDownText = rootView.findViewById(R.id.tempoStepDown);
-
- if (tempoCurrentText != null) {
- tempoCurrentText.setText(PlayerHelper.formatSpeed(tempo));
- }
- if (tempoMaximumText != null) {
- tempoMaximumText.setText(PlayerHelper.formatSpeed(MAXIMUM_PLAYBACK_VALUE));
- }
- if (tempoMinimumText != null) {
- tempoMinimumText.setText(PlayerHelper.formatSpeed(MINIMUM_PLAYBACK_VALUE));
- }
-
- if (tempoSlider != null) {
- tempoSlider.setMax(strategy.progressOf(MAXIMUM_PLAYBACK_VALUE));
- tempoSlider.setProgress(strategy.progressOf(tempo));
- tempoSlider.setOnSeekBarChangeListener(getOnTempoChangedListener());
- }
- }
-
- private void setupPitchControl(@NonNull final View rootView) {
- pitchSlider = rootView.findViewById(R.id.pitchSeekbar);
- final TextView pitchMinimumText = rootView.findViewById(R.id.pitchMinimumText);
- final TextView pitchMaximumText = rootView.findViewById(R.id.pitchMaximumText);
- pitchCurrentText = rootView.findViewById(R.id.pitchCurrentText);
- pitchStepDownText = rootView.findViewById(R.id.pitchStepDown);
- pitchStepUpText = rootView.findViewById(R.id.pitchStepUp);
-
- if (pitchCurrentText != null) {
- pitchCurrentText.setText(PlayerHelper.formatPitch(pitch));
- }
- if (pitchMaximumText != null) {
- pitchMaximumText.setText(PlayerHelper.formatPitch(MAXIMUM_PLAYBACK_VALUE));
- }
- if (pitchMinimumText != null) {
- pitchMinimumText.setText(PlayerHelper.formatPitch(MINIMUM_PLAYBACK_VALUE));
- }
-
- if (pitchSlider != null) {
- pitchSlider.setMax(strategy.progressOf(MAXIMUM_PLAYBACK_VALUE));
- pitchSlider.setProgress(strategy.progressOf(pitch));
- pitchSlider.setOnSeekBarChangeListener(getOnPitchChangedListener());
- }
- }
-
- private void setupSemitoneControl(@NonNull final View rootView) {
- semitoneSlider = rootView.findViewById(R.id.semitoneSeekbar);
- semitoneCurrentText = rootView.findViewById(R.id.semitoneCurrentText);
- semitoneStepDownText = rootView.findViewById(R.id.semitoneStepDown);
- semitoneStepUpText = rootView.findViewById(R.id.semitoneStepUp);
-
- if (semitoneCurrentText != null) {
- semitoneCurrentText.setText(getSignedSemitonesString(semitones));
- }
-
- if (semitoneSlider != null) {
- setSemitoneSlider(semitones);
- semitoneSlider.setOnSeekBarChangeListener(getOnSemitoneChangedListener());
- }
-
- }
-
- private void setupHookingControl(@NonNull final View rootView) {
- unhookingCheckbox = rootView.findViewById(R.id.unhookCheckbox);
- if (unhookingCheckbox != null) {
- // restores whether pitch and tempo are unhooked or not
- unhookingCheckbox.setChecked(PreferenceManager
- .getDefaultSharedPreferences(requireContext())
- .getBoolean(getString(R.string.playback_unhook_key), true));
-
- unhookingCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> {
- // saves whether pitch and tempo are unhooked or not
- PreferenceManager.getDefaultSharedPreferences(requireContext())
- .edit()
- .putBoolean(getString(R.string.playback_unhook_key), isChecked)
- .apply();
-
- if (!isChecked) {
- // when unchecked, slides back to the minimum of current tempo or pitch
- final double minimum = Math.min(getCurrentPitch(), getCurrentTempo());
- setSliders(minimum);
- setCurrentPlaybackParameters();
- }
- });
- }
- }
-
- private void setupSkipSilenceControl(@NonNull final View rootView) {
- skipSilenceCheckbox = rootView.findViewById(R.id.skipSilenceCheckbox);
- if (skipSilenceCheckbox != null) {
- skipSilenceCheckbox.setChecked(initialSkipSilence);
- skipSilenceCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) ->
- setCurrentPlaybackParameters());
- }
- }
-
- private void setupAdjustBySemitonesControl(@NonNull final View rootView) {
- adjustBySemitonesCheckbox = rootView.findViewById(R.id.adjustBySemitonesCheckbox);
- if (adjustBySemitonesCheckbox != null) {
- // restores whether semitone adjustment is used or not
- adjustBySemitonesCheckbox.setChecked(PreferenceManager
+ private void initUI(@NonNull final View rootView) {
+ // Tempo
+ tempoSlider = Objects.requireNonNull(rootView.findViewById(R.id.tempoSeekbar));
+ tempoCurrentText = Objects.requireNonNull(rootView.findViewById(R.id.tempoCurrentText));
+ tempoStepUpText = Objects.requireNonNull(rootView.findViewById(R.id.tempoStepUp));
+ tempoStepDownText = Objects.requireNonNull(rootView.findViewById(R.id.tempoStepDown));
+
+ setText(rootView, R.id.tempoMinimumText, PlayerHelper::formatSpeed, MINIMUM_PLAYBACK_VALUE);
+ setText(rootView, R.id.tempoMaximumText, PlayerHelper::formatSpeed, MAXIMUM_PLAYBACK_VALUE);
+
+ // Pitch
+ pitchSlider = Objects.requireNonNull(rootView.findViewById(R.id.pitchSeekbar));
+ pitchCurrentText = Objects.requireNonNull(rootView.findViewById(R.id.pitchCurrentText));
+ pitchStepUpText = Objects.requireNonNull(rootView.findViewById(R.id.pitchStepUp));
+ pitchStepDownText = Objects.requireNonNull(rootView.findViewById(R.id.pitchStepDown));
+
+ setText(rootView, R.id.pitchMinimumText, PlayerHelper::formatPitch, MINIMUM_PLAYBACK_VALUE);
+ setText(rootView, R.id.pitchMaximumText, PlayerHelper::formatPitch, MAXIMUM_PLAYBACK_VALUE);
+
+ // Steps
+ setupStepTextView(rootView, R.id.stepSizeOnePercent, STEP_1_PERCENT_VALUE);
+ setupStepTextView(rootView, R.id.stepSizeFivePercent, STEP_5_PERCENT_VALUE);
+ setupStepTextView(rootView, R.id.stepSizeTenPercent, STEP_10_PERCENT_VALUE);
+ setupStepTextView(rootView, R.id.stepSizeTwentyFivePercent, STEP_25_PERCENT_VALUE);
+ setupStepTextView(rootView, R.id.stepSizeOneHundredPercent, STEP_100_PERCENT_VALUE);
+
+ // Bottom controls
+ unhookingCheckbox =
+ Objects.requireNonNull(rootView.findViewById(R.id.unhookCheckbox));
+ skipSilenceCheckbox =
+ Objects.requireNonNull(rootView.findViewById(R.id.skipSilenceCheckbox));
+ }
+
+ private TextView setText(
+ final TextView textView,
+ final DoubleFunction formatter,
+ final double value
+ ) {
+ Objects.requireNonNull(textView).setText(formatter.apply(value));
+ return textView;
+ }
+
+ private TextView setText(
+ final View rootView,
+ @IdRes final int idRes,
+ final DoubleFunction formatter,
+ final double value
+ ) {
+ final TextView textView = rootView.findViewById(idRes);
+ setText(textView, formatter, value);
+ return textView;
+ }
+
+ private void setupStepTextView(
+ final View rootView,
+ @IdRes final int idRes,
+ final double stepSizeValue
+ ) {
+ setText(rootView, idRes, PlaybackParameterDialog::getPercentString, stepSizeValue)
+ .setOnClickListener(view -> setAndUpdateStepSize(stepSizeValue));
+ }
+
+ private void initUIData() {
+ // Tempo
+ tempoSlider.setMax(QUADRATIC_STRATEGY.progressOf(MAXIMUM_PLAYBACK_VALUE));
+ setAndUpdateTempo(tempo);
+ tempoSlider.setOnSeekBarChangeListener(
+ getTempoOrPitchSeekbarChangeListener(this::onTempoSliderUpdated));
+
+ registerOnStepClickListener(
+ tempoStepDownText, tempo, -1, this::onTempoSliderUpdated);
+ registerOnStepClickListener(
+ tempoStepUpText, tempo, 1, this::onTempoSliderUpdated);
+
+ // Pitch
+ pitchSlider.setMax(QUADRATIC_STRATEGY.progressOf(MAXIMUM_PLAYBACK_VALUE));
+ setAndUpdatePitch(pitch);
+ pitchSlider.setOnSeekBarChangeListener(
+ getTempoOrPitchSeekbarChangeListener(this::onPitchSliderUpdated));
+
+ registerOnStepClickListener(
+ pitchStepDownText, pitch, -1, this::onPitchSliderUpdated);
+ registerOnStepClickListener(
+ pitchStepUpText, pitch, 1, this::onPitchSliderUpdated);
+
+ // Steps
+ setAndUpdateStepSize(stepSize);
+
+ // Bottom controls
+ // restore whether pitch and tempo are unhooked or not
+ unhookingCheckbox.setChecked(PreferenceManager
.getDefaultSharedPreferences(requireContext())
- .getBoolean(getString(R.string.playback_adjust_by_semitones_key), true));
+ .getBoolean(getString(R.string.playback_unhook_key), true));
- // stores whether semitone adjustment is used or not
- adjustBySemitonesCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> {
- PreferenceManager.getDefaultSharedPreferences(requireContext())
+ unhookingCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> {
+ // save whether pitch and tempo are unhooked or not
+ PreferenceManager.getDefaultSharedPreferences(requireContext())
.edit()
- .putBoolean(getString(R.string.playback_adjust_by_semitones_key), isChecked)
+ .putBoolean(getString(R.string.playback_unhook_key), isChecked)
.apply();
- togglePitchSliderType(rootView);
- if (isChecked) {
- setPlaybackParameters(
- getCurrentTempo(),
- getCurrentPitch(),
- Integer.min(12,
- Integer.max(-12, percentToSemitones(getCurrentPitch())
- )),
- getCurrentSkipSilence()
- );
- setSemitoneSlider(Integer.min(12,
- Integer.max(-12, percentToSemitones(getCurrentPitch()))
- ));
- } else {
- setPlaybackParameters(
- getCurrentTempo(),
- semitonesToPercent(getCurrentSemitones()),
- getCurrentSemitones(),
- getCurrentSkipSilence()
- );
- setPitchSlider(semitonesToPercent(getCurrentSemitones()));
- }
- });
- }
- }
-
- private void setupStepSizeSelector(@NonNull final View rootView) {
- setStepSize(PreferenceManager
- .getDefaultSharedPreferences(requireContext())
- .getFloat(getString(R.string.adjustment_step_key), (float) DEFAULT_STEP));
-
- final TextView stepSizeOnePercentText = rootView.findViewById(R.id.stepSizeOnePercent);
- final TextView stepSizeFivePercentText = rootView.findViewById(R.id.stepSizeFivePercent);
- final TextView stepSizeTenPercentText = rootView.findViewById(R.id.stepSizeTenPercent);
- final TextView stepSizeTwentyFivePercentText = rootView
- .findViewById(R.id.stepSizeTwentyFivePercent);
- final TextView stepSizeOneHundredPercentText = rootView
- .findViewById(R.id.stepSizeOneHundredPercent);
-
- if (stepSizeOnePercentText != null) {
- stepSizeOnePercentText.setText(getPercentString(STEP_ONE_PERCENT_VALUE));
- stepSizeOnePercentText
- .setOnClickListener(view -> setStepSize(STEP_ONE_PERCENT_VALUE));
- }
- if (stepSizeFivePercentText != null) {
- stepSizeFivePercentText.setText(getPercentString(STEP_FIVE_PERCENT_VALUE));
- stepSizeFivePercentText
- .setOnClickListener(view -> setStepSize(STEP_FIVE_PERCENT_VALUE));
- }
-
- if (stepSizeTenPercentText != null) {
- stepSizeTenPercentText.setText(getPercentString(STEP_TEN_PERCENT_VALUE));
- stepSizeTenPercentText
- .setOnClickListener(view -> setStepSize(STEP_TEN_PERCENT_VALUE));
- }
-
- if (stepSizeTwentyFivePercentText != null) {
- stepSizeTwentyFivePercentText
- .setText(getPercentString(STEP_TWENTY_FIVE_PERCENT_VALUE));
- stepSizeTwentyFivePercentText
- .setOnClickListener(view -> setStepSize(STEP_TWENTY_FIVE_PERCENT_VALUE));
- }
+ if (!isChecked) {
+ // when unchecked, slide back to the minimum of current tempo or pitch
+ setSliders(Math.min(pitch, tempo));
+ }
+ });
- if (stepSizeOneHundredPercentText != null) {
- stepSizeOneHundredPercentText
- .setText(getPercentString(STEP_ONE_HUNDRED_PERCENT_VALUE));
- stepSizeOneHundredPercentText
- .setOnClickListener(view -> setStepSize(STEP_ONE_HUNDRED_PERCENT_VALUE));
- }
+ setAndUpdateSkipSilence(skipSilence);
+ skipSilenceCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> {
+ skipSilence = isChecked;
+ updateCallback();
+ });
}
- private void setupTempoStepSizeSelector(@NonNull final View rootView) {
- final TextView playbackStepTypeText = rootView.findViewById(R.id.playback_step_type);
- if (playbackStepTypeText != null) {
- playbackStepTypeText.setText(R.string.playback_tempo_step);
- }
- setupStepSizeSelector(rootView);
+ private void registerOnStepClickListener(
+ final TextView stepTextView,
+ final double currentValue,
+ final double direction, // -1 for step down, +1 for step up
+ final DoubleConsumer newValueConsumer
+ ) {
+ stepTextView.setOnClickListener(view ->
+ newValueConsumer.accept(currentValue * direction)
+ );
}
- private void setupCombinedStepSizeSelector(@NonNull final View rootView) {
- final TextView playbackStepTypeText = rootView.findViewById(R.id.playback_step_type);
- if (playbackStepTypeText != null) {
- playbackStepTypeText.setText(R.string.playback_step);
- }
- setupStepSizeSelector(rootView);
- }
+ private void setAndUpdateStepSize(final double newStepSize) {
+ this.stepSize = newStepSize;
- private void setStepSize(final double stepSize) {
- PreferenceManager.getDefaultSharedPreferences(requireContext())
- .edit()
- .putFloat(getString(R.string.adjustment_step_key), (float) stepSize)
- .apply();
-
- if (tempoStepUpText != null) {
- tempoStepUpText.setText(getStepUpPercentString(stepSize));
- tempoStepUpText.setOnClickListener(view -> {
- onTempoSliderUpdated(getCurrentTempo() + stepSize);
- setCurrentPlaybackParameters();
- });
- }
-
- if (tempoStepDownText != null) {
- tempoStepDownText.setText(getStepDownPercentString(stepSize));
- tempoStepDownText.setOnClickListener(view -> {
- onTempoSliderUpdated(getCurrentTempo() - stepSize);
- setCurrentPlaybackParameters();
- });
- }
-
- if (pitchStepUpText != null) {
- pitchStepUpText.setText(getStepUpPercentString(stepSize));
- pitchStepUpText.setOnClickListener(view -> {
- onPitchSliderUpdated(getCurrentPitch() + stepSize);
- setCurrentPlaybackParameters();
- });
- }
+ tempoStepUpText.setText(getStepUpPercentString(newStepSize));
+ tempoStepDownText.setText(getStepDownPercentString(newStepSize));
- if (pitchStepDownText != null) {
- pitchStepDownText.setText(getStepDownPercentString(stepSize));
- pitchStepDownText.setOnClickListener(view -> {
- onPitchSliderUpdated(getCurrentPitch() - stepSize);
- setCurrentPlaybackParameters();
- });
- }
-
- if (semitoneStepDownText != null) {
- semitoneStepDownText.setOnClickListener(view -> {
- onSemitoneSliderUpdated(getCurrentSemitones() - 1);
- setCurrentPlaybackParameters();
- });
- }
+ pitchStepUpText.setText(getStepUpPercentString(newStepSize));
+ pitchStepDownText.setText(getStepDownPercentString(newStepSize));
+ }
- if (semitoneStepUpText != null) {
- semitoneStepUpText.setOnClickListener(view -> {
- onSemitoneSliderUpdated(getCurrentSemitones() + 1);
- setCurrentPlaybackParameters();
- });
- }
+ private void setAndUpdateSkipSilence(final boolean newSkipSilence) {
+ this.skipSilence = newSkipSilence;
+ skipSilenceCheckbox.setChecked(newSkipSilence);
}
/*//////////////////////////////////////////////////////////////////////////
// Sliders
//////////////////////////////////////////////////////////////////////////*/
- private SimpleOnSeekBarChangeListener getOnTempoChangedListener() {
- return new SimpleOnSeekBarChangeListener() {
+ private SeekBar.OnSeekBarChangeListener getTempoOrPitchSeekbarChangeListener(
+ final DoubleConsumer newValueConsumer
+ ) {
+ return new SeekBar.OnSeekBarChangeListener() {
@Override
- public void onProgressChanged(@NonNull final SeekBar seekBar, final int progress,
+ public void onProgressChanged(final SeekBar seekBar, final int progress,
final boolean fromUser) {
- final double currentTempo = strategy.valueOf(progress);
- if (fromUser) {
- onTempoSliderUpdated(currentTempo);
- setCurrentPlaybackParameters();
+ if (fromUser) { // this change is first in chain
+ newValueConsumer.accept(QUADRATIC_STRATEGY.valueOf(progress));
+ updateCallback();
}
}
- };
- }
- private SimpleOnSeekBarChangeListener getOnPitchChangedListener() {
- return new SimpleOnSeekBarChangeListener() {
@Override
- public void onProgressChanged(@NonNull final SeekBar seekBar, final int progress,
- final boolean fromUser) {
- final double currentPitch = strategy.valueOf(progress);
- if (fromUser) { // this change is first in chain
- onPitchSliderUpdated(currentPitch);
- setCurrentPlaybackParameters();
- }
+ public void onStartTrackingTouch(final SeekBar seekBar) {
+ // Do nothing
}
- };
- }
- private SimpleOnSeekBarChangeListener getOnSemitoneChangedListener() {
- return new SimpleOnSeekBarChangeListener() {
@Override
- public void onProgressChanged(@NonNull final SeekBar seekBar, final int progress,
- final boolean fromUser) {
- // semitone slider supplies values 0 to 24, subtraction by 12 is required
- final int currentSemitones = progress - 12;
- if (fromUser) { // this change is first in chain
- onSemitoneSliderUpdated(currentSemitones);
- // line below also saves semitones as pitch percentages
- onPitchSliderUpdated(semitonesToPercent(currentSemitones));
- setCurrentPlaybackParameters();
- }
+ public void onStopTrackingTouch(final SeekBar seekBar) {
+ // Do nothing
}
};
}
@@ -545,7 +336,7 @@ private void onTempoSliderUpdated(final double newTempo) {
if (!unhookingCheckbox.isChecked()) {
setSliders(newTempo);
} else {
- setTempoSlider(newTempo);
+ setAndUpdateTempo(newTempo);
}
}
@@ -553,109 +344,53 @@ private void onPitchSliderUpdated(final double newPitch) {
if (!unhookingCheckbox.isChecked()) {
setSliders(newPitch);
} else {
- setPitchSlider(newPitch);
+ setAndUpdatePitch(newPitch);
}
}
- private void onSemitoneSliderUpdated(final int newSemitone) {
- setSemitoneSlider(newSemitone);
- }
-
private void setSliders(final double newValue) {
- setTempoSlider(newValue);
- setPitchSlider(newValue);
+ setAndUpdateTempo(newValue);
+ setAndUpdatePitch(newValue);
}
- private void setTempoSlider(final double newTempo) {
- if (tempoSlider == null) {
- return;
- }
- tempoSlider.setProgress(strategy.progressOf(newTempo));
+ private void setAndUpdateTempo(final double newTempo) {
+ this.tempo = newTempo;
+ tempoSlider.setProgress(QUADRATIC_STRATEGY.progressOf(tempo));
+ setText(tempoCurrentText, PlayerHelper::formatSpeed, tempo);
}
- private void setPitchSlider(final double newPitch) {
- if (pitchSlider == null) {
- return;
- }
- pitchSlider.setProgress(strategy.progressOf(newPitch));
- }
-
- private void setSemitoneSlider(final int newSemitone) {
- if (semitoneSlider == null) {
- return;
- }
- semitoneSlider.setProgress(newSemitone + 12);
+ private void setAndUpdatePitch(final double newPitch) {
+ this.pitch = newPitch;
+ pitchSlider.setProgress(QUADRATIC_STRATEGY.progressOf(pitch));
+ setText(pitchCurrentText, PlayerHelper::formatPitch, pitch);
}
/*//////////////////////////////////////////////////////////////////////////
// Helper
//////////////////////////////////////////////////////////////////////////*/
- private void setCurrentPlaybackParameters() {
- if (getCurrentAdjustBySemitones()) {
- setPlaybackParameters(
- getCurrentTempo(),
- semitonesToPercent(getCurrentSemitones()),
- getCurrentSemitones(),
- getCurrentSkipSilence()
- );
- } else {
- setPlaybackParameters(
- getCurrentTempo(),
- getCurrentPitch(),
- percentToSemitones(getCurrentPitch()),
- getCurrentSkipSilence()
- );
+ private void updateCallback() {
+ if (callback == null) {
+ return;
}
- }
-
- private void setPlaybackParameters(final double newTempo, final double newPitch,
- final int newSemitones, final boolean skipSilence) {
- if (callback != null && tempoCurrentText != null
- && pitchCurrentText != null && semitoneCurrentText != null) {
- if (DEBUG) {
- Log.d(TAG, "Setting playback parameters to "
- + "tempo=[" + newTempo + "], "
- + "pitch=[" + newPitch + "], "
- + "semitones=[" + newSemitones + "]");
- }
-
- tempoCurrentText.setText(PlayerHelper.formatSpeed(newTempo));
- pitchCurrentText.setText(PlayerHelper.formatPitch(newPitch));
- semitoneCurrentText.setText(getSignedSemitonesString(newSemitones));
- callback.onPlaybackParameterChanged((float) newTempo, (float) newPitch, skipSilence);
+ if (DEBUG) {
+ Log.d(TAG, "Updating callback: "
+ + "tempo = [" + tempo + "], "
+ + "pitch = [" + pitch + "], "
+ + "skipSilence = [" + skipSilence + "]"
+ );
}
- }
-
- private double getCurrentTempo() {
- return tempoSlider == null ? tempo : strategy.valueOf(tempoSlider.getProgress());
- }
-
- private double getCurrentPitch() {
- return pitchSlider == null ? pitch : strategy.valueOf(pitchSlider.getProgress());
- }
-
- private int getCurrentSemitones() {
- // semitoneSlider is absolute, that's why - 12
- return semitoneSlider == null ? semitones : semitoneSlider.getProgress() - 12;
- }
-
- private boolean getCurrentSkipSilence() {
- return skipSilenceCheckbox != null && skipSilenceCheckbox.isChecked();
- }
-
- private boolean getCurrentAdjustBySemitones() {
- return adjustBySemitonesCheckbox != null && adjustBySemitonesCheckbox.isChecked();
+ callback.onPlaybackParameterChanged((float) tempo, (float) pitch, skipSilence);
}
@NonNull
private static String getStepUpPercentString(final double percent) {
- return STEP_UP_SIGN + getPercentString(percent);
+ return '+' + getPercentString(percent);
}
@NonNull
private static String getStepDownPercentString(final double percent) {
- return STEP_DOWN_SIGN + getPercentString(percent);
+ return '-' + getPercentString(percent);
}
@NonNull
@@ -663,21 +398,8 @@ private static String getPercentString(final double percent) {
return PlayerHelper.formatPitch(percent);
}
- @NonNull
- private static String getSignedSemitonesString(final int semitones) {
- return semitones > 0 ? "+" + semitones : "" + semitones;
- }
-
public interface Callback {
void onPlaybackParameterChanged(float playbackTempo, float playbackPitch,
boolean playbackSkipSilence);
}
-
- public double semitonesToPercent(final int inSemitones) {
- return Math.pow(2, inSemitones / 12.0);
- }
-
- public int percentToSemitones(final double inPercent) {
- return (int) Math.round(12 * Math.log(inPercent) / Math.log(2));
- }
}
From 4cdf6eda2cf3d9073d9ccad7c339bfb1856189c2 Mon Sep 17 00:00:00 2001
From: litetex <40789489+litetex@users.noreply.github.com>
Date: Mon, 28 Feb 2022 20:45:23 +0100
Subject: [PATCH 003/240] Use viewbinding
---
.../helper/PlaybackParameterDialog.java | 124 ++++++------------
1 file changed, 42 insertions(+), 82 deletions(-)
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java
index b72b7ea7b0f..e1874fec0b9 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java
@@ -7,12 +7,10 @@
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
-import android.view.View;
-import android.widget.CheckBox;
+import android.view.LayoutInflater;
import android.widget.SeekBar;
import android.widget.TextView;
-import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
@@ -20,6 +18,7 @@
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R;
+import org.schabi.newpipe.databinding.DialogPlaybackParameterBinding;
import org.schabi.newpipe.util.SliderStrategy;
import java.util.Objects;
@@ -72,18 +71,7 @@ public class PlaybackParameterDialog extends DialogFragment {
@State
boolean skipSilence = DEFAULT_SKIP_SILENCE;
- private SeekBar tempoSlider;
- private TextView tempoCurrentText;
- private TextView tempoStepDownText;
- private TextView tempoStepUpText;
-
- private SeekBar pitchSlider;
- private TextView pitchCurrentText;
- private TextView pitchStepDownText;
- private TextView pitchStepUpText;
-
- private CheckBox unhookingCheckbox;
- private CheckBox skipSilenceCheckbox;
+ private DialogPlaybackParameterBinding binding;
public static PlaybackParameterDialog newInstance(
final double playbackTempo,
@@ -110,7 +98,7 @@ public static PlaybackParameterDialog newInstance(
//////////////////////////////////////////////////////////////////////////*/
@Override
- public void onAttach(final Context context) {
+ public void onAttach(@NonNull final Context context) {
super.onAttach(context);
if (context instanceof Callback) {
callback = (Callback) context;
@@ -120,7 +108,7 @@ public void onAttach(final Context context) {
}
@Override
- public void onSaveInstanceState(final Bundle outState) {
+ public void onSaveInstanceState(@NonNull final Bundle outState) {
super.onSaveInstanceState(outState);
Icepick.saveInstanceState(this, outState);
}
@@ -135,12 +123,12 @@ public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) {
assureCorrectAppLanguage(getContext());
Icepick.restoreInstanceState(this, savedInstanceState);
- final View view = View.inflate(getContext(), R.layout.dialog_playback_parameter, null);
- initUI(view);
+ binding = DialogPlaybackParameterBinding.inflate(LayoutInflater.from(getContext()));
+ initUI();
initUIData();
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireActivity())
- .setView(view)
+ .setView(binding.getRoot())
.setCancelable(true)
.setNegativeButton(R.string.cancel, (dialogInterface, i) -> {
setAndUpdateTempo(initialTempo);
@@ -163,37 +151,21 @@ public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) {
// Control Views
//////////////////////////////////////////////////////////////////////////*/
- private void initUI(@NonNull final View rootView) {
+ private void initUI() {
// Tempo
- tempoSlider = Objects.requireNonNull(rootView.findViewById(R.id.tempoSeekbar));
- tempoCurrentText = Objects.requireNonNull(rootView.findViewById(R.id.tempoCurrentText));
- tempoStepUpText = Objects.requireNonNull(rootView.findViewById(R.id.tempoStepUp));
- tempoStepDownText = Objects.requireNonNull(rootView.findViewById(R.id.tempoStepDown));
-
- setText(rootView, R.id.tempoMinimumText, PlayerHelper::formatSpeed, MINIMUM_PLAYBACK_VALUE);
- setText(rootView, R.id.tempoMaximumText, PlayerHelper::formatSpeed, MAXIMUM_PLAYBACK_VALUE);
+ setText(binding.tempoMinimumText, PlayerHelper::formatSpeed, MINIMUM_PLAYBACK_VALUE);
+ setText(binding.tempoMaximumText, PlayerHelper::formatSpeed, MAXIMUM_PLAYBACK_VALUE);
// Pitch
- pitchSlider = Objects.requireNonNull(rootView.findViewById(R.id.pitchSeekbar));
- pitchCurrentText = Objects.requireNonNull(rootView.findViewById(R.id.pitchCurrentText));
- pitchStepUpText = Objects.requireNonNull(rootView.findViewById(R.id.pitchStepUp));
- pitchStepDownText = Objects.requireNonNull(rootView.findViewById(R.id.pitchStepDown));
-
- setText(rootView, R.id.pitchMinimumText, PlayerHelper::formatPitch, MINIMUM_PLAYBACK_VALUE);
- setText(rootView, R.id.pitchMaximumText, PlayerHelper::formatPitch, MAXIMUM_PLAYBACK_VALUE);
+ setText(binding.pitchMinimumText, PlayerHelper::formatPitch, MINIMUM_PLAYBACK_VALUE);
+ setText(binding.pitchMaximumText, PlayerHelper::formatPitch, MAXIMUM_PLAYBACK_VALUE);
// Steps
- setupStepTextView(rootView, R.id.stepSizeOnePercent, STEP_1_PERCENT_VALUE);
- setupStepTextView(rootView, R.id.stepSizeFivePercent, STEP_5_PERCENT_VALUE);
- setupStepTextView(rootView, R.id.stepSizeTenPercent, STEP_10_PERCENT_VALUE);
- setupStepTextView(rootView, R.id.stepSizeTwentyFivePercent, STEP_25_PERCENT_VALUE);
- setupStepTextView(rootView, R.id.stepSizeOneHundredPercent, STEP_100_PERCENT_VALUE);
-
- // Bottom controls
- unhookingCheckbox =
- Objects.requireNonNull(rootView.findViewById(R.id.unhookCheckbox));
- skipSilenceCheckbox =
- Objects.requireNonNull(rootView.findViewById(R.id.skipSilenceCheckbox));
+ setupStepTextView(binding.stepSizeOnePercent, STEP_1_PERCENT_VALUE);
+ setupStepTextView(binding.stepSizeFivePercent, STEP_5_PERCENT_VALUE);
+ setupStepTextView(binding.stepSizeTenPercent, STEP_10_PERCENT_VALUE);
+ setupStepTextView(binding.stepSizeTwentyFivePercent, STEP_25_PERCENT_VALUE);
+ setupStepTextView(binding.stepSizeOneHundredPercent, STEP_100_PERCENT_VALUE);
}
private TextView setText(
@@ -205,59 +177,47 @@ private TextView setText(
return textView;
}
- private TextView setText(
- final View rootView,
- @IdRes final int idRes,
- final DoubleFunction formatter,
- final double value
- ) {
- final TextView textView = rootView.findViewById(idRes);
- setText(textView, formatter, value);
- return textView;
- }
-
private void setupStepTextView(
- final View rootView,
- @IdRes final int idRes,
+ final TextView textView,
final double stepSizeValue
) {
- setText(rootView, idRes, PlaybackParameterDialog::getPercentString, stepSizeValue)
+ setText(textView, PlaybackParameterDialog::getPercentString, stepSizeValue)
.setOnClickListener(view -> setAndUpdateStepSize(stepSizeValue));
}
private void initUIData() {
// Tempo
- tempoSlider.setMax(QUADRATIC_STRATEGY.progressOf(MAXIMUM_PLAYBACK_VALUE));
+ binding.tempoSeekbar.setMax(QUADRATIC_STRATEGY.progressOf(MAXIMUM_PLAYBACK_VALUE));
setAndUpdateTempo(tempo);
- tempoSlider.setOnSeekBarChangeListener(
+ binding.tempoSeekbar.setOnSeekBarChangeListener(
getTempoOrPitchSeekbarChangeListener(this::onTempoSliderUpdated));
registerOnStepClickListener(
- tempoStepDownText, tempo, -1, this::onTempoSliderUpdated);
+ binding.tempoStepDown, tempo, -1, this::onTempoSliderUpdated);
registerOnStepClickListener(
- tempoStepUpText, tempo, 1, this::onTempoSliderUpdated);
+ binding.tempoStepUp, tempo, 1, this::onTempoSliderUpdated);
// Pitch
- pitchSlider.setMax(QUADRATIC_STRATEGY.progressOf(MAXIMUM_PLAYBACK_VALUE));
+ binding.pitchSeekbar.setMax(QUADRATIC_STRATEGY.progressOf(MAXIMUM_PLAYBACK_VALUE));
setAndUpdatePitch(pitch);
- pitchSlider.setOnSeekBarChangeListener(
+ binding.pitchSeekbar.setOnSeekBarChangeListener(
getTempoOrPitchSeekbarChangeListener(this::onPitchSliderUpdated));
registerOnStepClickListener(
- pitchStepDownText, pitch, -1, this::onPitchSliderUpdated);
+ binding.pitchStepDown, pitch, -1, this::onPitchSliderUpdated);
registerOnStepClickListener(
- pitchStepUpText, pitch, 1, this::onPitchSliderUpdated);
+ binding.pitchStepUp, pitch, 1, this::onPitchSliderUpdated);
// Steps
setAndUpdateStepSize(stepSize);
// Bottom controls
// restore whether pitch and tempo are unhooked or not
- unhookingCheckbox.setChecked(PreferenceManager
+ binding.unhookCheckbox.setChecked(PreferenceManager
.getDefaultSharedPreferences(requireContext())
.getBoolean(getString(R.string.playback_unhook_key), true));
- unhookingCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> {
+ binding.unhookCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> {
// save whether pitch and tempo are unhooked or not
PreferenceManager.getDefaultSharedPreferences(requireContext())
.edit()
@@ -271,7 +231,7 @@ private void initUIData() {
});
setAndUpdateSkipSilence(skipSilence);
- skipSilenceCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> {
+ binding.skipSilenceCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> {
skipSilence = isChecked;
updateCallback();
});
@@ -291,16 +251,16 @@ private void registerOnStepClickListener(
private void setAndUpdateStepSize(final double newStepSize) {
this.stepSize = newStepSize;
- tempoStepUpText.setText(getStepUpPercentString(newStepSize));
- tempoStepDownText.setText(getStepDownPercentString(newStepSize));
+ binding.tempoStepUp.setText(getStepUpPercentString(newStepSize));
+ binding.tempoStepDown.setText(getStepDownPercentString(newStepSize));
- pitchStepUpText.setText(getStepUpPercentString(newStepSize));
- pitchStepDownText.setText(getStepDownPercentString(newStepSize));
+ binding.pitchStepUp.setText(getStepUpPercentString(newStepSize));
+ binding.pitchStepDown.setText(getStepDownPercentString(newStepSize));
}
private void setAndUpdateSkipSilence(final boolean newSkipSilence) {
this.skipSilence = newSkipSilence;
- skipSilenceCheckbox.setChecked(newSkipSilence);
+ binding.skipSilenceCheckbox.setChecked(newSkipSilence);
}
/*//////////////////////////////////////////////////////////////////////////
@@ -333,7 +293,7 @@ public void onStopTrackingTouch(final SeekBar seekBar) {
}
private void onTempoSliderUpdated(final double newTempo) {
- if (!unhookingCheckbox.isChecked()) {
+ if (!binding.unhookCheckbox.isChecked()) {
setSliders(newTempo);
} else {
setAndUpdateTempo(newTempo);
@@ -341,7 +301,7 @@ private void onTempoSliderUpdated(final double newTempo) {
}
private void onPitchSliderUpdated(final double newPitch) {
- if (!unhookingCheckbox.isChecked()) {
+ if (!binding.unhookCheckbox.isChecked()) {
setSliders(newPitch);
} else {
setAndUpdatePitch(newPitch);
@@ -355,14 +315,14 @@ private void setSliders(final double newValue) {
private void setAndUpdateTempo(final double newTempo) {
this.tempo = newTempo;
- tempoSlider.setProgress(QUADRATIC_STRATEGY.progressOf(tempo));
- setText(tempoCurrentText, PlayerHelper::formatSpeed, tempo);
+ binding.tempoSeekbar.setProgress(QUADRATIC_STRATEGY.progressOf(tempo));
+ setText(binding.tempoCurrentText, PlayerHelper::formatSpeed, tempo);
}
private void setAndUpdatePitch(final double newPitch) {
this.pitch = newPitch;
- pitchSlider.setProgress(QUADRATIC_STRATEGY.progressOf(pitch));
- setText(pitchCurrentText, PlayerHelper::formatPitch, pitch);
+ binding.pitchSeekbar.setProgress(QUADRATIC_STRATEGY.progressOf(pitch));
+ setText(binding.pitchCurrentText, PlayerHelper::formatPitch, pitch);
}
/*//////////////////////////////////////////////////////////////////////////
From 6e0c3804097a1e3749d329c82e8cca3c72d7ec18 Mon Sep 17 00:00:00 2001
From: litetex <40789489+litetex@users.noreply.github.com>
Date: Mon, 28 Feb 2022 21:02:43 +0100
Subject: [PATCH 004/240] Remove redundant attributes
---
.../res/layout/dialog_playback_parameter.xml | 27 -------------------
1 file changed, 27 deletions(-)
diff --git a/app/src/main/res/layout/dialog_playback_parameter.xml b/app/src/main/res/layout/dialog_playback_parameter.xml
index 862b2ea671f..27cf0dbd696 100644
--- a/app/src/main/res/layout/dialog_playback_parameter.xml
+++ b/app/src/main/res/layout/dialog_playback_parameter.xml
@@ -39,7 +39,6 @@
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_alignParentStart="true"
- android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:background="?attr/selectableItemBackground"
android:clickable="true"
@@ -57,9 +56,7 @@
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
android:layout_toStartOf="@id/tempoStepUp"
- android:layout_toLeftOf="@id/tempoStepUp"
android:layout_toEndOf="@id/tempoStepDown"
- android:layout_toRightOf="@id/tempoStepDown"
android:orientation="horizontal">
Date: Mon, 28 Feb 2022 21:04:24 +0100
Subject: [PATCH 005/240] Remove invalid parameters
---
app/src/main/res/layout/dialog_playback_parameter.xml | 2 --
1 file changed, 2 deletions(-)
diff --git a/app/src/main/res/layout/dialog_playback_parameter.xml b/app/src/main/res/layout/dialog_playback_parameter.xml
index 27cf0dbd696..87c26b831da 100644
--- a/app/src/main/res/layout/dialog_playback_parameter.xml
+++ b/app/src/main/res/layout/dialog_playback_parameter.xml
@@ -463,8 +463,6 @@
android:id="@+id/adjustBySemitonesCheckbox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_below="@id/skipSilenceCheckbox"
- android:layout_centerHorizontal="true"
android:checked="false"
android:clickable="true"
android:focusable="true"
From a4c083e7f98c535993c9e22d9d3ae99d5c410a0c Mon Sep 17 00:00:00 2001
From: litetex <40789489+litetex@users.noreply.github.com>
Date: Tue, 1 Mar 2022 20:52:48 +0100
Subject: [PATCH 006/240] Rework dialog
* De-Duplicated some fields
* Use a container for the pitch controls
* Name pitch related elements correctly
---
.../res/layout/dialog_playback_parameter.xml | 302 +++++++++---------
1 file changed, 153 insertions(+), 149 deletions(-)
diff --git a/app/src/main/res/layout/dialog_playback_parameter.xml b/app/src/main/res/layout/dialog_playback_parameter.xml
index 87c26b831da..47394b724ee 100644
--- a/app/src/main/res/layout/dialog_playback_parameter.xml
+++ b/app/src/main/res/layout/dialog_playback_parameter.xml
@@ -29,7 +29,7 @@
@@ -146,203 +146,207 @@
android:textStyle="bold" />
-
-
+ android:layout_marginTop="3dp">
-
-
+ tools:text="-5%" />
+
+
+
+
+
+
+
+
+
+
+
-
-
+ tools:text="+5%" />
-
-
-
-
-
-
-
-
-
+ tools:ignore="HardcodedText" />
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
Date: Tue, 1 Mar 2022 21:19:30 +0100
Subject: [PATCH 007/240] Shrunk dialog a bit
---
app/src/main/res/layout/dialog_playback_parameter.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/src/main/res/layout/dialog_playback_parameter.xml b/app/src/main/res/layout/dialog_playback_parameter.xml
index 47394b724ee..1ab9a95e953 100644
--- a/app/src/main/res/layout/dialog_playback_parameter.xml
+++ b/app/src/main/res/layout/dialog_playback_parameter.xml
@@ -356,7 +356,7 @@
From dae5aa38a83a892f43a3589a21bada854601273d Mon Sep 17 00:00:00 2001
From: litetex <40789489+litetex@users.noreply.github.com>
Date: Wed, 2 Mar 2022 20:58:14 +0100
Subject: [PATCH 008/240] Fine tuned dialog (no scrollers in fullscreen on 5in
phone)
---
.../res/layout/dialog_playback_parameter.xml | 24 +++++++++----------
1 file changed, 12 insertions(+), 12 deletions(-)
diff --git a/app/src/main/res/layout/dialog_playback_parameter.xml b/app/src/main/res/layout/dialog_playback_parameter.xml
index 1ab9a95e953..75269ffd3e0 100644
--- a/app/src/main/res/layout/dialog_playback_parameter.xml
+++ b/app/src/main/res/layout/dialog_playback_parameter.xml
@@ -31,7 +31,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/tempoControlText"
- android:layout_marginTop="3dp"
+ android:layout_marginTop="1dp"
android:orientation="horizontal">
@@ -129,9 +129,9 @@
android:layout_height="1dp"
android:layout_below="@id/tempoControl"
android:layout_marginStart="12dp"
- android:layout_marginTop="6dp"
- android:layout_marginEnd="6dp"
- android:layout_marginBottom="6dp"
+ android:layout_marginTop="4dp"
+ android:layout_marginEnd="12dp"
+ android:layout_marginBottom="4dp"
android:background="?attr/separator_color" />
+ android:layout_marginTop="1dp">
@@ -319,7 +319,7 @@
android:layout_height="wrap_content"
android:layout_below="@+id/pitchSemitoneCurrentText"
android:max="24"
- android:paddingBottom="4dp"
+ android:paddingBottom="2dp"
android:progress="12" />
@@ -348,9 +348,9 @@
android:layout_height="1dp"
android:layout_below="@+id/pitchControlContainer"
android:layout_marginStart="12dp"
- android:layout_marginTop="6dp"
+ android:layout_marginTop="4dp"
android:layout_marginEnd="12dp"
- android:layout_marginBottom="6dp"
+ android:layout_marginBottom="4dp"
android:background="?attr/separator_color" />
Date: Wed, 2 Mar 2022 21:00:19 +0100
Subject: [PATCH 009/240] Reworked/Implemented PlaybackParameterDialog
functionallity
* Add support for semitones
* Fixed some minor bugs
* Improved some methods
---
.../helper/PlaybackParameterDialog.java | 312 +++++++++++++-----
.../player/helper/PlayerSemitoneHelper.java | 37 +++
2 files changed, 260 insertions(+), 89 deletions(-)
create mode 100644 app/src/main/java/org/schabi/newpipe/player/helper/PlayerSemitoneHelper.java
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java
index e1874fec0b9..709216ece47 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java
@@ -8,11 +8,14 @@
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.CheckBox;
import android.widget.SeekBar;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import androidx.preference.PreferenceManager;
@@ -22,8 +25,10 @@
import org.schabi.newpipe.util.SliderStrategy;
import java.util.Objects;
+import java.util.function.Consumer;
import java.util.function.DoubleConsumer;
import java.util.function.DoubleFunction;
+import java.util.function.DoubleSupplier;
import icepick.Icepick;
import icepick.State;
@@ -32,8 +37,8 @@ public class PlaybackParameterDialog extends DialogFragment {
private static final String TAG = "PlaybackParameterDialog";
// Minimum allowable range in ExoPlayer
- private static final double MINIMUM_PLAYBACK_VALUE = 0.10f;
- private static final double MAXIMUM_PLAYBACK_VALUE = 3.00f;
+ private static final double MIN_PLAYBACK_VALUE = 0.10f;
+ private static final double MAX_PLAYBACK_VALUE = 3.00f;
private static final double STEP_1_PERCENT_VALUE = 0.01f;
private static final double STEP_5_PERCENT_VALUE = 0.05f;
@@ -42,30 +47,42 @@ public class PlaybackParameterDialog extends DialogFragment {
private static final double STEP_100_PERCENT_VALUE = 1.00f;
private static final double DEFAULT_TEMPO = 1.00f;
- private static final double DEFAULT_PITCH = 1.00f;
+ private static final double DEFAULT_PITCH_PERCENT = 1.00f;
private static final double DEFAULT_STEP = STEP_25_PERCENT_VALUE;
private static final boolean DEFAULT_SKIP_SILENCE = false;
private static final SliderStrategy QUADRATIC_STRATEGY = new SliderStrategy.Quadratic(
- MINIMUM_PLAYBACK_VALUE,
- MAXIMUM_PLAYBACK_VALUE,
+ MIN_PLAYBACK_VALUE,
+ MAX_PLAYBACK_VALUE,
1.00f,
10_000);
+ private static final SliderStrategy SEMITONE_STRATEGY = new SliderStrategy() {
+ @Override
+ public int progressOf(final double value) {
+ return PlayerSemitoneHelper.percentToSemitones(value) + 12;
+ }
+
+ @Override
+ public double valueOf(final int progress) {
+ return PlayerSemitoneHelper.semitonesToPercent(progress - 12);
+ }
+ };
+
@Nullable
private Callback callback;
@State
double initialTempo = DEFAULT_TEMPO;
@State
- double initialPitch = DEFAULT_PITCH;
+ double initialPitchPercent = DEFAULT_PITCH_PERCENT;
@State
boolean initialSkipSilence = DEFAULT_SKIP_SILENCE;
@State
double tempo = DEFAULT_TEMPO;
@State
- double pitch = DEFAULT_PITCH;
+ double pitchPercent = DEFAULT_PITCH_PERCENT;
@State
double stepSize = DEFAULT_STEP;
@State
@@ -83,11 +100,11 @@ public static PlaybackParameterDialog newInstance(
dialog.callback = callback;
dialog.initialTempo = playbackTempo;
- dialog.initialPitch = playbackPitch;
+ dialog.initialPitchPercent = playbackPitch;
dialog.initialSkipSilence = playbackSkipSilence;
dialog.tempo = dialog.initialTempo;
- dialog.pitch = dialog.initialPitch;
+ dialog.pitchPercent = dialog.initialPitchPercent;
dialog.skipSilence = dialog.initialSkipSilence;
return dialog;
@@ -125,20 +142,19 @@ public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) {
binding = DialogPlaybackParameterBinding.inflate(LayoutInflater.from(getContext()));
initUI();
- initUIData();
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireActivity())
.setView(binding.getRoot())
.setCancelable(true)
.setNegativeButton(R.string.cancel, (dialogInterface, i) -> {
setAndUpdateTempo(initialTempo);
- setAndUpdatePitch(initialPitch);
+ setAndUpdatePitch(initialPitchPercent);
setAndUpdateSkipSilence(initialSkipSilence);
updateCallback();
})
.setNeutralButton(R.string.playback_reset, (dialogInterface, i) -> {
setAndUpdateTempo(DEFAULT_TEMPO);
- setAndUpdatePitch(DEFAULT_PITCH);
+ setAndUpdatePitch(DEFAULT_PITCH_PERCENT);
setAndUpdateSkipSilence(DEFAULT_SKIP_SILENCE);
updateCallback();
})
@@ -153,12 +169,63 @@ public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) {
private void initUI() {
// Tempo
- setText(binding.tempoMinimumText, PlayerHelper::formatSpeed, MINIMUM_PLAYBACK_VALUE);
- setText(binding.tempoMaximumText, PlayerHelper::formatSpeed, MAXIMUM_PLAYBACK_VALUE);
+ setText(binding.tempoMinimumText, PlayerHelper::formatSpeed, MIN_PLAYBACK_VALUE);
+ setText(binding.tempoMaximumText, PlayerHelper::formatSpeed, MAX_PLAYBACK_VALUE);
- // Pitch
- setText(binding.pitchMinimumText, PlayerHelper::formatPitch, MINIMUM_PLAYBACK_VALUE);
- setText(binding.pitchMaximumText, PlayerHelper::formatPitch, MAXIMUM_PLAYBACK_VALUE);
+ binding.tempoSeekbar.setMax(QUADRATIC_STRATEGY.progressOf(MAX_PLAYBACK_VALUE));
+ setAndUpdateTempo(tempo);
+ binding.tempoSeekbar.setOnSeekBarChangeListener(
+ getTempoOrPitchSeekbarChangeListener(
+ QUADRATIC_STRATEGY,
+ this::onTempoSliderUpdated));
+
+ registerOnStepClickListener(
+ binding.tempoStepDown,
+ () -> tempo,
+ -1,
+ this::onTempoSliderUpdated);
+ registerOnStepClickListener(
+ binding.tempoStepUp,
+ () -> tempo,
+ 1,
+ this::onTempoSliderUpdated);
+
+ // Pitch - Percent
+ setText(binding.pitchPercentMinimumText, PlayerHelper::formatPitch, MIN_PLAYBACK_VALUE);
+ setText(binding.pitchPercentMaximumText, PlayerHelper::formatPitch, MAX_PLAYBACK_VALUE);
+
+ binding.pitchPercentSeekbar.setMax(QUADRATIC_STRATEGY.progressOf(MAX_PLAYBACK_VALUE));
+ setAndUpdatePitch(pitchPercent);
+ binding.pitchPercentSeekbar.setOnSeekBarChangeListener(
+ getTempoOrPitchSeekbarChangeListener(
+ QUADRATIC_STRATEGY,
+ this::onPitchPercentSliderUpdated));
+
+ registerOnStepClickListener(
+ binding.pitchPercentStepDown,
+ () -> pitchPercent,
+ -1,
+ this::onPitchPercentSliderUpdated);
+ registerOnStepClickListener(
+ binding.pitchPercentStepUp,
+ () -> pitchPercent,
+ 1,
+ this::onPitchPercentSliderUpdated);
+
+ // Pitch - Semitone
+ binding.pitchSemitoneSeekbar.setOnSeekBarChangeListener(
+ getTempoOrPitchSeekbarChangeListener(
+ SEMITONE_STRATEGY,
+ this::onPitchPercentSliderUpdated));
+
+ registerOnSemitoneStepClickListener(
+ binding.pitchSemitoneStepDown,
+ -1,
+ this::onPitchPercentSliderUpdated);
+ registerOnSemitoneStepClickListener(
+ binding.pitchSemitoneStepUp,
+ 1,
+ this::onPitchPercentSliderUpdated);
// Steps
setupStepTextView(binding.stepSizeOnePercent, STEP_1_PERCENT_VALUE);
@@ -166,6 +233,34 @@ private void initUI() {
setupStepTextView(binding.stepSizeTenPercent, STEP_10_PERCENT_VALUE);
setupStepTextView(binding.stepSizeTwentyFivePercent, STEP_25_PERCENT_VALUE);
setupStepTextView(binding.stepSizeOneHundredPercent, STEP_100_PERCENT_VALUE);
+
+ setAndUpdateStepSize(stepSize);
+
+ // Bottom controls
+ bindCheckboxWithBoolPref(
+ binding.unhookCheckbox,
+ R.string.playback_unhook_key,
+ true,
+ isChecked -> {
+ if (!isChecked) {
+ // when unchecked, slide back to the minimum of current tempo or pitch
+ setSliders(Math.min(pitchPercent, tempo));
+ updateCallback();
+ }
+ });
+
+ setAndUpdateSkipSilence(skipSilence);
+ binding.skipSilenceCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> {
+ skipSilence = isChecked;
+ updateCallback();
+ });
+
+ bindCheckboxWithBoolPref(
+ binding.adjustBySemitonesCheckbox,
+ R.string.playback_adjust_by_semitones_key,
+ false,
+ this::showPitchSemitonesOrPercent
+ );
}
private TextView setText(
@@ -177,6 +272,31 @@ private TextView setText(
return textView;
}
+ private void registerOnStepClickListener(
+ final TextView stepTextView,
+ final DoubleSupplier currentValueSupplier,
+ final double direction, // -1 for step down, +1 for step up
+ final DoubleConsumer newValueConsumer
+ ) {
+ stepTextView.setOnClickListener(view -> {
+ newValueConsumer.accept(
+ currentValueSupplier.getAsDouble() + 1 * stepSize * direction);
+ updateCallback();
+ });
+ }
+
+ private void registerOnSemitoneStepClickListener(
+ final TextView stepTextView,
+ final int direction, // -1 for step down, +1 for step up
+ final DoubleConsumer newValueConsumer
+ ) {
+ stepTextView.setOnClickListener(view -> {
+ newValueConsumer.accept(PlayerSemitoneHelper.semitonesToPercent(
+ PlayerSemitoneHelper.percentToSemitones(this.pitchPercent) + direction));
+ updateCallback();
+ });
+ }
+
private void setupStepTextView(
final TextView textView,
final double stepSizeValue
@@ -185,82 +305,71 @@ private void setupStepTextView(
.setOnClickListener(view -> setAndUpdateStepSize(stepSizeValue));
}
- private void initUIData() {
- // Tempo
- binding.tempoSeekbar.setMax(QUADRATIC_STRATEGY.progressOf(MAXIMUM_PLAYBACK_VALUE));
- setAndUpdateTempo(tempo);
- binding.tempoSeekbar.setOnSeekBarChangeListener(
- getTempoOrPitchSeekbarChangeListener(this::onTempoSliderUpdated));
-
- registerOnStepClickListener(
- binding.tempoStepDown, tempo, -1, this::onTempoSliderUpdated);
- registerOnStepClickListener(
- binding.tempoStepUp, tempo, 1, this::onTempoSliderUpdated);
+ private void setAndUpdateStepSize(final double newStepSize) {
+ this.stepSize = newStepSize;
- // Pitch
- binding.pitchSeekbar.setMax(QUADRATIC_STRATEGY.progressOf(MAXIMUM_PLAYBACK_VALUE));
- setAndUpdatePitch(pitch);
- binding.pitchSeekbar.setOnSeekBarChangeListener(
- getTempoOrPitchSeekbarChangeListener(this::onPitchSliderUpdated));
+ binding.tempoStepUp.setText(getStepUpPercentString(newStepSize));
+ binding.tempoStepDown.setText(getStepDownPercentString(newStepSize));
- registerOnStepClickListener(
- binding.pitchStepDown, pitch, -1, this::onPitchSliderUpdated);
- registerOnStepClickListener(
- binding.pitchStepUp, pitch, 1, this::onPitchSliderUpdated);
+ binding.pitchPercentStepUp.setText(getStepUpPercentString(newStepSize));
+ binding.pitchPercentStepDown.setText(getStepDownPercentString(newStepSize));
+ }
- // Steps
- setAndUpdateStepSize(stepSize);
+ private void setAndUpdateSkipSilence(final boolean newSkipSilence) {
+ this.skipSilence = newSkipSilence;
+ binding.skipSilenceCheckbox.setChecked(newSkipSilence);
+ }
- // Bottom controls
- // restore whether pitch and tempo are unhooked or not
- binding.unhookCheckbox.setChecked(PreferenceManager
+ private void bindCheckboxWithBoolPref(
+ @NonNull final CheckBox checkBox,
+ @StringRes final int resId,
+ final boolean defaultValue,
+ @Nullable final Consumer onInitialValueOrValueChange
+ ) {
+ final boolean prefValue = PreferenceManager
.getDefaultSharedPreferences(requireContext())
- .getBoolean(getString(R.string.playback_unhook_key), true));
+ .getBoolean(getString(resId), defaultValue);
+
+ checkBox.setChecked(prefValue);
- binding.unhookCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> {
+ if (onInitialValueOrValueChange != null) {
+ onInitialValueOrValueChange.accept(prefValue);
+ }
+
+ checkBox.setOnCheckedChangeListener((compoundButton, isChecked) -> {
// save whether pitch and tempo are unhooked or not
PreferenceManager.getDefaultSharedPreferences(requireContext())
.edit()
- .putBoolean(getString(R.string.playback_unhook_key), isChecked)
+ .putBoolean(getString(resId), isChecked)
.apply();
- if (!isChecked) {
- // when unchecked, slide back to the minimum of current tempo or pitch
- setSliders(Math.min(pitch, tempo));
+ if (onInitialValueOrValueChange != null) {
+ onInitialValueOrValueChange.accept(isChecked);
}
});
-
- setAndUpdateSkipSilence(skipSilence);
- binding.skipSilenceCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> {
- skipSilence = isChecked;
- updateCallback();
- });
- }
-
- private void registerOnStepClickListener(
- final TextView stepTextView,
- final double currentValue,
- final double direction, // -1 for step down, +1 for step up
- final DoubleConsumer newValueConsumer
- ) {
- stepTextView.setOnClickListener(view ->
- newValueConsumer.accept(currentValue * direction)
- );
}
- private void setAndUpdateStepSize(final double newStepSize) {
- this.stepSize = newStepSize;
-
- binding.tempoStepUp.setText(getStepUpPercentString(newStepSize));
- binding.tempoStepDown.setText(getStepDownPercentString(newStepSize));
-
- binding.pitchStepUp.setText(getStepUpPercentString(newStepSize));
- binding.pitchStepDown.setText(getStepDownPercentString(newStepSize));
- }
-
- private void setAndUpdateSkipSilence(final boolean newSkipSilence) {
- this.skipSilence = newSkipSilence;
- binding.skipSilenceCheckbox.setChecked(newSkipSilence);
+ private void showPitchSemitonesOrPercent(final boolean semitones) {
+ binding.pitchPercentControl.setVisibility(semitones ? View.GONE : View.VISIBLE);
+ binding.pitchSemitoneControl.setVisibility(semitones ? View.VISIBLE : View.GONE);
+
+ if (semitones) {
+ // Recalculate pitch percent when changing to semitone
+ // (as it could be an invalid semitone value)
+ final double newPitchPercent = calcValidPitch(pitchPercent);
+
+ // If the values differ set the new pitch
+ if (this.pitchPercent != newPitchPercent) {
+ if (DEBUG) {
+ Log.d(TAG, "Bringing pitchPercent to correct corresponding semitone: "
+ + "currentPitchPercent = " + pitchPercent + ", "
+ + "newPitchPercent = " + newPitchPercent
+ );
+ }
+ this.onPitchPercentSliderUpdated(newPitchPercent);
+ updateCallback();
+ }
+ }
}
/*//////////////////////////////////////////////////////////////////////////
@@ -268,14 +377,15 @@ private void setAndUpdateSkipSilence(final boolean newSkipSilence) {
//////////////////////////////////////////////////////////////////////////*/
private SeekBar.OnSeekBarChangeListener getTempoOrPitchSeekbarChangeListener(
+ final SliderStrategy sliderStrategy,
final DoubleConsumer newValueConsumer
) {
return new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(final SeekBar seekBar, final int progress,
final boolean fromUser) {
- if (fromUser) { // this change is first in chain
- newValueConsumer.accept(QUADRATIC_STRATEGY.valueOf(progress));
+ if (fromUser) { // ensure that the user triggered the change
+ newValueConsumer.accept(sliderStrategy.valueOf(progress));
updateCallback();
}
}
@@ -300,7 +410,7 @@ private void onTempoSliderUpdated(final double newTempo) {
}
}
- private void onPitchSliderUpdated(final double newPitch) {
+ private void onPitchPercentSliderUpdated(final double newPitch) {
if (!binding.unhookCheckbox.isChecked()) {
setSliders(newPitch);
} else {
@@ -314,15 +424,39 @@ private void setSliders(final double newValue) {
}
private void setAndUpdateTempo(final double newTempo) {
- this.tempo = newTempo;
+ this.tempo = calcValidTempo(newTempo);
+
binding.tempoSeekbar.setProgress(QUADRATIC_STRATEGY.progressOf(tempo));
setText(binding.tempoCurrentText, PlayerHelper::formatSpeed, tempo);
}
private void setAndUpdatePitch(final double newPitch) {
- this.pitch = newPitch;
- binding.pitchSeekbar.setProgress(QUADRATIC_STRATEGY.progressOf(pitch));
- setText(binding.pitchCurrentText, PlayerHelper::formatPitch, pitch);
+ this.pitchPercent = calcValidPitch(newPitch);
+
+ binding.pitchPercentSeekbar.setProgress(QUADRATIC_STRATEGY.progressOf(pitchPercent));
+ binding.pitchSemitoneSeekbar.setProgress(SEMITONE_STRATEGY.progressOf(pitchPercent));
+ setText(binding.pitchPercentCurrentText,
+ PlayerHelper::formatPitch,
+ pitchPercent);
+ setText(binding.pitchSemitoneCurrentText,
+ PlayerSemitoneHelper::formatPitchSemitones,
+ pitchPercent);
+ }
+
+ private double calcValidTempo(final double newTempo) {
+ return Math.max(MIN_PLAYBACK_VALUE, Math.min(MAX_PLAYBACK_VALUE, newTempo));
+ }
+
+ private double calcValidPitch(final double newPitch) {
+ final double calcPitch =
+ Math.max(MIN_PLAYBACK_VALUE, Math.min(MAX_PLAYBACK_VALUE, newPitch));
+
+ if (!binding.adjustBySemitonesCheckbox.isChecked()) {
+ return calcPitch;
+ }
+
+ return PlayerSemitoneHelper.semitonesToPercent(
+ PlayerSemitoneHelper.percentToSemitones(calcPitch));
}
/*//////////////////////////////////////////////////////////////////////////
@@ -335,12 +469,12 @@ private void updateCallback() {
}
if (DEBUG) {
Log.d(TAG, "Updating callback: "
- + "tempo = [" + tempo + "], "
- + "pitch = [" + pitch + "], "
- + "skipSilence = [" + skipSilence + "]"
+ + "tempo = " + tempo + ", "
+ + "pitchPercent = " + pitchPercent + ", "
+ + "skipSilence = " + skipSilence
);
}
- callback.onPlaybackParameterChanged((float) tempo, (float) pitch, skipSilence);
+ callback.onPlaybackParameterChanged((float) tempo, (float) pitchPercent, skipSilence);
}
@NonNull
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerSemitoneHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerSemitoneHelper.java
new file mode 100644
index 00000000000..abbcc2c822f
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerSemitoneHelper.java
@@ -0,0 +1,37 @@
+package org.schabi.newpipe.player.helper;
+
+/**
+ * Converts between percent and 12-tone equal temperament semitones.
+ *
+ * @see
+ *
+ * Wikipedia: Equal temperament#Twelve-tone equal temperament
+ *
+ */
+public final class PlayerSemitoneHelper {
+ public static final int TONES = 12;
+
+ private PlayerSemitoneHelper() {
+ // No impl
+ }
+
+ public static String formatPitchSemitones(final double percent) {
+ return formatPitchSemitones(percentToSemitones(percent));
+ }
+
+ public static String formatPitchSemitones(final int semitones) {
+ return semitones > 0 ? "+" + semitones : "" + semitones;
+ }
+
+ public static double semitonesToPercent(final int semitones) {
+ return Math.pow(2, ensureSemitonesInRange(semitones) / (double) TONES);
+ }
+
+ public static int percentToSemitones(final double percent) {
+ return ensureSemitonesInRange((int) Math.round(TONES * Math.log(percent) / Math.log(2)));
+ }
+
+ private static int ensureSemitonesInRange(final int semitones) {
+ return Math.max(-TONES, Math.min(TONES, semitones));
+ }
+}
From 321cf8bf7d7a419913f58697cb4cb7e5630212d7 Mon Sep 17 00:00:00 2001
From: litetex <40789489+litetex@users.noreply.github.com>
Date: Fri, 4 Mar 2022 21:33:21 +0100
Subject: [PATCH 010/240] Fine tuned dialog
---
.../res/layout/dialog_playback_parameter.xml | 36 +++++++++++--------
1 file changed, 22 insertions(+), 14 deletions(-)
diff --git a/app/src/main/res/layout/dialog_playback_parameter.xml b/app/src/main/res/layout/dialog_playback_parameter.xml
index 75269ffd3e0..640475f392a 100644
--- a/app/src/main/res/layout/dialog_playback_parameter.xml
+++ b/app/src/main/res/layout/dialog_playback_parameter.xml
@@ -1,5 +1,6 @@
+ android:orientation="horizontal"
+ tools:visibility="gone">
+ tools:text="0"
+ tools:ignore="RelativeOverlap" />
+ android:textColor="?attr/colorAccent"
+ tools:text="1%" />
+ android:textColor="?attr/colorAccent"
+ tools:text="5%" />
+ android:textColor="?attr/colorAccent"
+ tools:text="10%" />
+ android:textColor="?attr/colorAccent"
+ tools:text="25%" />
-
+ android:textColor="?attr/colorAccent"
+ tools:text="100%" />
Date: Fri, 4 Mar 2022 21:34:45 +0100
Subject: [PATCH 011/240] Code improvements regarding stepSize
---
.../helper/PlaybackParameterDialog.java | 34 ++++++++-----------
1 file changed, 15 insertions(+), 19 deletions(-)
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java
index 709216ece47..eab64e48349 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java
@@ -84,8 +84,6 @@ public double valueOf(final int progress) {
@State
double pitchPercent = DEFAULT_PITCH_PERCENT;
@State
- double stepSize = DEFAULT_STEP;
- @State
boolean skipSilence = DEFAULT_SKIP_SILENCE;
private DialogPlaybackParameterBinding binding;
@@ -228,13 +226,10 @@ private void initUI() {
this::onPitchPercentSliderUpdated);
// Steps
- setupStepTextView(binding.stepSizeOnePercent, STEP_1_PERCENT_VALUE);
- setupStepTextView(binding.stepSizeFivePercent, STEP_5_PERCENT_VALUE);
- setupStepTextView(binding.stepSizeTenPercent, STEP_10_PERCENT_VALUE);
- setupStepTextView(binding.stepSizeTwentyFivePercent, STEP_25_PERCENT_VALUE);
- setupStepTextView(binding.stepSizeOneHundredPercent, STEP_100_PERCENT_VALUE);
-
- setAndUpdateStepSize(stepSize);
+ getStepSizeComponentMappings()
+ .forEach(this::setupStepTextView);
+ // Initialize UI
+ setStepSizeToUI(getCurrentStepSize());
// Bottom controls
bindCheckboxWithBoolPref(
@@ -263,13 +258,12 @@ private void initUI() {
);
}
- private TextView setText(
+ private void setText(
final TextView textView,
final DoubleFunction formatter,
final double value
) {
Objects.requireNonNull(textView).setText(formatter.apply(value));
- return textView;
}
private void registerOnStepClickListener(
@@ -280,7 +274,7 @@ private void registerOnStepClickListener(
) {
stepTextView.setOnClickListener(view -> {
newValueConsumer.accept(
- currentValueSupplier.getAsDouble() + 1 * stepSize * direction);
+ currentValueSupplier.getAsDouble() + 1 * getCurrentStepSize() * direction);
updateCallback();
});
}
@@ -315,16 +309,22 @@ private void setAndUpdateStepSize(final double newStepSize) {
binding.pitchPercentStepDown.setText(getStepDownPercentString(newStepSize));
}
+ private double getCurrentStepSize() {
+ return PreferenceManager.getDefaultSharedPreferences(requireContext())
+ .getFloat(getString(R.string.adjustment_step_key), (float) DEFAULT_STEP);
+ }
+
private void setAndUpdateSkipSilence(final boolean newSkipSilence) {
this.skipSilence = newSkipSilence;
binding.skipSilenceCheckbox.setChecked(newSkipSilence);
}
+ @SuppressWarnings("SameParameterValue") // this method was written to be reusable
private void bindCheckboxWithBoolPref(
@NonNull final CheckBox checkBox,
@StringRes final int resId,
final boolean defaultValue,
- @Nullable final Consumer onInitialValueOrValueChange
+ @NonNull final Consumer onInitialValueOrValueChange
) {
final boolean prefValue = PreferenceManager
.getDefaultSharedPreferences(requireContext())
@@ -332,9 +332,7 @@ private void bindCheckboxWithBoolPref(
checkBox.setChecked(prefValue);
- if (onInitialValueOrValueChange != null) {
- onInitialValueOrValueChange.accept(prefValue);
- }
+ onInitialValueOrValueChange.accept(prefValue);
checkBox.setOnCheckedChangeListener((compoundButton, isChecked) -> {
// save whether pitch and tempo are unhooked or not
@@ -343,9 +341,7 @@ private void bindCheckboxWithBoolPref(
.putBoolean(getString(resId), isChecked)
.apply();
- if (onInitialValueOrValueChange != null) {
- onInitialValueOrValueChange.accept(isChecked);
- }
+ onInitialValueOrValueChange.accept(isChecked);
});
}
From 4b0653658273f456ddbc56a86b1695073d982895 Mon Sep 17 00:00:00 2001
From: litetex <40789489+litetex@users.noreply.github.com>
Date: Fri, 4 Mar 2022 21:37:11 +0100
Subject: [PATCH 012/240] Reworked switching to semitones
Using an expandable Tab-like component instead of a combobox
---
.../schabi/newpipe/local/feed/FeedFragment.kt | 16 +-
.../helper/PlaybackParameterDialog.java | 167 ++++++++++++++----
.../schabi/newpipe/util/DrawableResolver.kt | 26 +++
.../res/layout/dialog_playback_parameter.xml | 59 +++++--
app/src/main/res/values/strings.xml | 2 +
5 files changed, 208 insertions(+), 62 deletions(-)
create mode 100644 app/src/main/java/org/schabi/newpipe/util/DrawableResolver.kt
diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt
index e97629f31a9..e8e78fedae9 100644
--- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt
+++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt
@@ -25,7 +25,6 @@ import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.graphics.Typeface
-import android.graphics.drawable.Drawable
import android.graphics.drawable.LayerDrawable
import android.os.Bundle
import android.os.Parcelable
@@ -37,7 +36,6 @@ import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.Button
-import androidx.annotation.AttrRes
import androidx.annotation.Nullable
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.content.res.AppCompatResources
@@ -77,6 +75,7 @@ import org.schabi.newpipe.local.feed.item.StreamItem
import org.schabi.newpipe.local.feed.service.FeedLoadService
import org.schabi.newpipe.local.subscription.SubscriptionManager
import org.schabi.newpipe.util.DeviceUtils
+import org.schabi.newpipe.util.DrawableResolver.Companion.resolveDrawable
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.NavigationHelper
import org.schabi.newpipe.util.ThemeHelper.getGridSpanCountStreams
@@ -579,19 +578,6 @@ class FeedFragment : BaseStateFragment() {
lastNewItemsCount = highlightCount
}
- private fun resolveDrawable(context: Context, @AttrRes attrResId: Int): Drawable? {
- return androidx.core.content.ContextCompat.getDrawable(
- context,
- android.util.TypedValue().apply {
- context.theme.resolveAttribute(
- attrResId,
- this,
- true
- )
- }.resourceId
- )
- }
-
private fun showNewItemsLoaded() {
tryGetNewItemsLoadedButton()?.clearAnimation()
tryGetNewItemsLoadedButton()
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java
index eab64e48349..902222cc5b3 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java
@@ -1,10 +1,14 @@
package org.schabi.newpipe.player.helper;
+import static org.schabi.newpipe.ktx.ViewUtils.animateRotation;
import static org.schabi.newpipe.player.Player.DEBUG;
+import static org.schabi.newpipe.util.DrawableResolver.resolveDrawable;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
import android.app.Dialog;
import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
@@ -22,8 +26,11 @@
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.DialogPlaybackParameterBinding;
+import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.util.SliderStrategy;
+import java.util.HashMap;
+import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.DoubleConsumer;
@@ -40,6 +47,9 @@ public class PlaybackParameterDialog extends DialogFragment {
private static final double MIN_PLAYBACK_VALUE = 0.10f;
private static final double MAX_PLAYBACK_VALUE = 3.00f;
+ private static final boolean PITCH_CTRL_MODE_PERCENT = false;
+ private static final boolean PITCH_CTRL_MODE_SEMITONE = true;
+
private static final double STEP_1_PERCENT_VALUE = 0.01f;
private static final double STEP_5_PERCENT_VALUE = 0.05f;
private static final double STEP_10_PERCENT_VALUE = 0.10f;
@@ -188,6 +198,22 @@ private void initUI() {
1,
this::onTempoSliderUpdated);
+ // Pitch
+ binding.pitchToogleControlModes.setOnClickListener(v -> {
+ final boolean isCurrentlyVisible =
+ binding.pitchControlModeTabs.getVisibility() == View.GONE;
+ binding.pitchControlModeTabs.setVisibility(isCurrentlyVisible
+ ? View.VISIBLE
+ : View.GONE);
+ animateRotation(binding.pitchToogleControlModes,
+ Player.DEFAULT_CONTROLS_DURATION,
+ isCurrentlyVisible ? 180 : 0);
+ });
+
+ getPitchControlModeComponentMappings()
+ .forEach(this::setupPitchControlModeTextView);
+ changePitchControlMode(isCurrentPitchControlModeSemitone());
+
// Pitch - Percent
setText(binding.pitchPercentMinimumText, PlayerHelper::formatPitch, MIN_PLAYBACK_VALUE);
setText(binding.pitchPercentMaximumText, PlayerHelper::formatPitch, MAX_PLAYBACK_VALUE);
@@ -249,13 +275,6 @@ private void initUI() {
skipSilence = isChecked;
updateCallback();
});
-
- bindCheckboxWithBoolPref(
- binding.adjustBySemitonesCheckbox,
- R.string.playback_adjust_by_semitones_key,
- false,
- this::showPitchSemitonesOrPercent
- );
}
private void setText(
@@ -291,17 +310,114 @@ private void registerOnSemitoneStepClickListener(
});
}
+ private void setupPitchControlModeTextView(
+ final boolean semitones,
+ final TextView textView
+ ) {
+ textView.setOnClickListener(view -> {
+ PreferenceManager.getDefaultSharedPreferences(requireContext())
+ .edit()
+ .putBoolean(getString(R.string.playback_adjust_by_semitones_key), semitones)
+ .apply();
+
+ changePitchControlMode(semitones);
+ });
+ }
+
+ private Map getPitchControlModeComponentMappings() {
+ final Map mappings = new HashMap<>();
+ mappings.put(PITCH_CTRL_MODE_PERCENT, binding.pitchControlModePercent);
+ mappings.put(PITCH_CTRL_MODE_SEMITONE, binding.pitchControlModeSemitone);
+ return mappings;
+ }
+
+ private void changePitchControlMode(final boolean semitones) {
+ // Bring all textviews into a normal state
+ final Map pitchCtrlModeComponentMapping =
+ getPitchControlModeComponentMappings();
+ pitchCtrlModeComponentMapping.forEach((v, textView) -> textView.setBackground(
+ resolveDrawable(requireContext(), R.attr.selectableItemBackground)));
+
+ // Mark the selected textview
+ final TextView textView = pitchCtrlModeComponentMapping.get(semitones);
+ if (textView != null) {
+ textView.setBackground(new LayerDrawable(new Drawable[]{
+ resolveDrawable(requireContext(), R.attr.dashed_border),
+ resolveDrawable(requireContext(), R.attr.selectableItemBackground)
+ }));
+ }
+
+ // Show or hide component
+ binding.pitchPercentControl.setVisibility(semitones ? View.GONE : View.VISIBLE);
+ binding.pitchSemitoneControl.setVisibility(semitones ? View.VISIBLE : View.GONE);
+
+ if (semitones) {
+ // Recalculate pitch percent when changing to semitone
+ // (as it could be an invalid semitone value)
+ final double newPitchPercent = calcValidPitch(pitchPercent);
+
+ // If the values differ set the new pitch
+ if (this.pitchPercent != newPitchPercent) {
+ if (DEBUG) {
+ Log.d(TAG, "Bringing pitchPercent to correct corresponding semitone: "
+ + "currentPitchPercent = " + pitchPercent + ", "
+ + "newPitchPercent = " + newPitchPercent
+ );
+ }
+ this.onPitchPercentSliderUpdated(newPitchPercent);
+ updateCallback();
+ }
+ }
+ }
+
+ private boolean isCurrentPitchControlModeSemitone() {
+ return PreferenceManager.getDefaultSharedPreferences(requireContext())
+ .getBoolean(
+ getString(R.string.playback_adjust_by_semitones_key),
+ PITCH_CTRL_MODE_PERCENT);
+ }
+
private void setupStepTextView(
- final TextView textView,
- final double stepSizeValue
+ final double stepSizeValue,
+ final TextView textView
) {
- setText(textView, PlaybackParameterDialog::getPercentString, stepSizeValue)
- .setOnClickListener(view -> setAndUpdateStepSize(stepSizeValue));
+ setText(textView, PlaybackParameterDialog::getPercentString, stepSizeValue);
+ textView.setOnClickListener(view -> {
+ PreferenceManager.getDefaultSharedPreferences(requireContext())
+ .edit()
+ .putFloat(getString(R.string.adjustment_step_key), (float) stepSizeValue)
+ .apply();
+
+ setStepSizeToUI(stepSizeValue);
+ });
}
- private void setAndUpdateStepSize(final double newStepSize) {
- this.stepSize = newStepSize;
+ private Map getStepSizeComponentMappings() {
+ final Map mappings = new HashMap<>();
+ mappings.put(STEP_1_PERCENT_VALUE, binding.stepSizeOnePercent);
+ mappings.put(STEP_5_PERCENT_VALUE, binding.stepSizeFivePercent);
+ mappings.put(STEP_10_PERCENT_VALUE, binding.stepSizeTenPercent);
+ mappings.put(STEP_25_PERCENT_VALUE, binding.stepSizeTwentyFivePercent);
+ mappings.put(STEP_100_PERCENT_VALUE, binding.stepSizeOneHundredPercent);
+ return mappings;
+ }
+
+ private void setStepSizeToUI(final double newStepSize) {
+ // Bring all textviews into a normal state
+ final Map stepSiteComponentMapping = getStepSizeComponentMappings();
+ stepSiteComponentMapping.forEach((v, textView) -> textView.setBackground(
+ resolveDrawable(requireContext(), R.attr.selectableItemBackground)));
+
+ // Mark the selected textview
+ final TextView textView = stepSiteComponentMapping.get(newStepSize);
+ if (textView != null) {
+ textView.setBackground(new LayerDrawable(new Drawable[]{
+ resolveDrawable(requireContext(), R.attr.dashed_border),
+ resolveDrawable(requireContext(), R.attr.selectableItemBackground)
+ }));
+ }
+ // Bind to the corresponding control components
binding.tempoStepUp.setText(getStepUpPercentString(newStepSize));
binding.tempoStepDown.setText(getStepDownPercentString(newStepSize));
@@ -345,29 +461,6 @@ private void bindCheckboxWithBoolPref(
});
}
- private void showPitchSemitonesOrPercent(final boolean semitones) {
- binding.pitchPercentControl.setVisibility(semitones ? View.GONE : View.VISIBLE);
- binding.pitchSemitoneControl.setVisibility(semitones ? View.VISIBLE : View.GONE);
-
- if (semitones) {
- // Recalculate pitch percent when changing to semitone
- // (as it could be an invalid semitone value)
- final double newPitchPercent = calcValidPitch(pitchPercent);
-
- // If the values differ set the new pitch
- if (this.pitchPercent != newPitchPercent) {
- if (DEBUG) {
- Log.d(TAG, "Bringing pitchPercent to correct corresponding semitone: "
- + "currentPitchPercent = " + pitchPercent + ", "
- + "newPitchPercent = " + newPitchPercent
- );
- }
- this.onPitchPercentSliderUpdated(newPitchPercent);
- updateCallback();
- }
- }
- }
-
/*//////////////////////////////////////////////////////////////////////////
// Sliders
//////////////////////////////////////////////////////////////////////////*/
@@ -447,7 +540,7 @@ private double calcValidPitch(final double newPitch) {
final double calcPitch =
Math.max(MIN_PLAYBACK_VALUE, Math.min(MAX_PLAYBACK_VALUE, newPitch));
- if (!binding.adjustBySemitonesCheckbox.isChecked()) {
+ if (!isCurrentPitchControlModeSemitone()) {
return calcPitch;
}
diff --git a/app/src/main/java/org/schabi/newpipe/util/DrawableResolver.kt b/app/src/main/java/org/schabi/newpipe/util/DrawableResolver.kt
new file mode 100644
index 00000000000..50f875257fc
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/util/DrawableResolver.kt
@@ -0,0 +1,26 @@
+package org.schabi.newpipe.util
+
+import android.content.Context
+import android.graphics.drawable.Drawable
+import androidx.annotation.AttrRes
+
+/**
+ * Utility class for resolving [Drawables](Drawable)
+ */
+class DrawableResolver {
+ companion object {
+ @JvmStatic
+ fun resolveDrawable(context: Context, @AttrRes attrResId: Int): Drawable? {
+ return androidx.core.content.ContextCompat.getDrawable(
+ context,
+ android.util.TypedValue().apply {
+ context.theme.resolveAttribute(
+ attrResId,
+ this,
+ true
+ )
+ }.resourceId
+ )
+ }
+ }
+}
diff --git a/app/src/main/res/layout/dialog_playback_parameter.xml b/app/src/main/res/layout/dialog_playback_parameter.xml
index 640475f392a..e402f4fb170 100644
--- a/app/src/main/res/layout/dialog_playback_parameter.xml
+++ b/app/src/main/res/layout/dialog_playback_parameter.xml
@@ -146,11 +146,59 @@
android:textColor="?attr/colorAccent"
android:textStyle="bold" />
+
+
+
+
+
+
+
+
+
+
-
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 792e6414b52..af2921ccaa0 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -501,6 +501,8 @@
StepTempo stepReset
+ Percent
+ SemitoneIn order to comply with the European General Data Protection Regulation (GDPR), we hereby draw your attention to NewPipe\'s privacy policy. Please read it carefully.
\nYou must accept it to send us the bug report.
From 20602889be38e38c25e6022f56be83860ef34313 Mon Sep 17 00:00:00 2001
From: litetex <40789489+litetex@users.noreply.github.com>
Date: Fri, 4 Mar 2022 22:02:39 +0100
Subject: [PATCH 013/240] Added some doc and abstracted more methods
---
.../helper/PlaybackParameterDialog.java | 30 +++++++++++++++++--
1 file changed, 27 insertions(+), 3 deletions(-)
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java
index 902222cc5b3..4ab8f9248b4 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java
@@ -172,7 +172,7 @@ public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) {
}
/*//////////////////////////////////////////////////////////////////////////
- // Control Views
+ // UI Initialization and Control
//////////////////////////////////////////////////////////////////////////*/
private void initUI() {
@@ -265,8 +265,7 @@ private void initUI() {
isChecked -> {
if (!isChecked) {
// when unchecked, slide back to the minimum of current tempo or pitch
- setSliders(Math.min(pitchPercent, tempo));
- updateCallback();
+ ensureHookIsValidAndUpdateCallBack();
}
});
@@ -277,6 +276,8 @@ private void initUI() {
});
}
+ // -- General formatting --
+
private void setText(
final TextView textView,
final DoubleFunction formatter,
@@ -285,6 +286,8 @@ private void setText(
Objects.requireNonNull(textView).setText(formatter.apply(value));
}
+ // -- Steps --
+
private void registerOnStepClickListener(
final TextView stepTextView,
final DoubleSupplier currentValueSupplier,
@@ -310,6 +313,8 @@ private void registerOnSemitoneStepClickListener(
});
}
+ // -- Pitch --
+
private void setupPitchControlModeTextView(
final boolean semitones,
final TextView textView
@@ -367,6 +372,9 @@ private void changePitchControlMode(final boolean semitones) {
this.onPitchPercentSliderUpdated(newPitchPercent);
updateCallback();
}
+ } else if (!binding.unhookCheckbox.isChecked()) {
+ // When changing to percent it's possible that tempo is != pitch
+ ensureHookIsValidAndUpdateCallBack();
}
}
@@ -377,6 +385,8 @@ private boolean isCurrentPitchControlModeSemitone() {
PITCH_CTRL_MODE_PERCENT);
}
+ // -- Steps (Set) --
+
private void setupStepTextView(
final double stepSizeValue,
final TextView textView
@@ -430,6 +440,8 @@ private double getCurrentStepSize() {
.getFloat(getString(R.string.adjustment_step_key), (float) DEFAULT_STEP);
}
+ // -- Additional options --
+
private void setAndUpdateSkipSilence(final boolean newSkipSilence) {
this.skipSilence = newSkipSilence;
binding.skipSilenceCheckbox.setChecked(newSkipSilence);
@@ -461,6 +473,18 @@ private void bindCheckboxWithBoolPref(
});
}
+ /**
+ * Ensures that the slider hook is valid and if not sets and updates the sliders accordingly.
+ *
+ * You have to ensure by yourself that the hooking is active.
+ */
+ private void ensureHookIsValidAndUpdateCallBack() {
+ if (tempo != pitchPercent) {
+ setSliders(Math.min(tempo, pitchPercent));
+ updateCallback();
+ }
+ }
+
/*//////////////////////////////////////////////////////////////////////////
// Sliders
//////////////////////////////////////////////////////////////////////////*/
From 1b8c517e3ea1924787eeff075a575414a5f3b544 Mon Sep 17 00:00:00 2001
From: litetex <40789489+litetex@users.noreply.github.com>
Date: Fri, 4 Mar 2022 22:21:17 +0100
Subject: [PATCH 014/240] Removed unused strings
---
app/src/main/res/values/strings.xml | 2 --
1 file changed, 2 deletions(-)
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index af2921ccaa0..1f87ae9fb6c 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -497,9 +497,7 @@
PitchUnhook (may cause distortion)Fast-forward during silence
- Adjust pitch by musical semitonesStep
- Tempo stepResetPercentSemitone
From 44dada9e60b23f500c3904af8578a874e78c0c5a Mon Sep 17 00:00:00 2001
From: litetex <40789489+litetex@users.noreply.github.com>
Date: Sun, 6 Mar 2022 16:10:42 +0100
Subject: [PATCH 015/240] Use better Kotlin syntax
From the PR review
---
.../schabi/newpipe/local/feed/FeedFragment.kt | 2 +-
.../schabi/newpipe/util/DrawableResolver.kt | 29 +++++++++----------
2 files changed, 15 insertions(+), 16 deletions(-)
diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt
index e8e78fedae9..55810284f2f 100644
--- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt
+++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt
@@ -75,7 +75,7 @@ import org.schabi.newpipe.local.feed.item.StreamItem
import org.schabi.newpipe.local.feed.service.FeedLoadService
import org.schabi.newpipe.local.subscription.SubscriptionManager
import org.schabi.newpipe.util.DeviceUtils
-import org.schabi.newpipe.util.DrawableResolver.Companion.resolveDrawable
+import org.schabi.newpipe.util.DrawableResolver.resolveDrawable
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.NavigationHelper
import org.schabi.newpipe.util.ThemeHelper.getGridSpanCountStreams
diff --git a/app/src/main/java/org/schabi/newpipe/util/DrawableResolver.kt b/app/src/main/java/org/schabi/newpipe/util/DrawableResolver.kt
index 50f875257fc..ccc9e7dd4af 100644
--- a/app/src/main/java/org/schabi/newpipe/util/DrawableResolver.kt
+++ b/app/src/main/java/org/schabi/newpipe/util/DrawableResolver.kt
@@ -2,25 +2,24 @@ package org.schabi.newpipe.util
import android.content.Context
import android.graphics.drawable.Drawable
+import android.util.TypedValue
import androidx.annotation.AttrRes
/**
* Utility class for resolving [Drawables](Drawable)
*/
-class DrawableResolver {
- companion object {
- @JvmStatic
- fun resolveDrawable(context: Context, @AttrRes attrResId: Int): Drawable? {
- return androidx.core.content.ContextCompat.getDrawable(
- context,
- android.util.TypedValue().apply {
- context.theme.resolveAttribute(
- attrResId,
- this,
- true
- )
- }.resourceId
- )
- }
+object DrawableResolver {
+ @JvmStatic
+ fun resolveDrawable(context: Context, @AttrRes attrResId: Int): Drawable? {
+ return androidx.core.content.ContextCompat.getDrawable(
+ context,
+ TypedValue().apply {
+ context.theme.resolveAttribute(
+ attrResId,
+ this,
+ true
+ )
+ }.resourceId
+ )
}
}
From b9190eddfe1563cc5269caf2ca90a997db85a920 Mon Sep 17 00:00:00 2001
From: litetex <40789489+litetex@users.noreply.github.com>
Date: Mon, 7 Mar 2022 20:30:25 +0100
Subject: [PATCH 016/240] Update DrawableResolver.kt
Nicer import :wink:
---
app/src/main/java/org/schabi/newpipe/util/DrawableResolver.kt | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/app/src/main/java/org/schabi/newpipe/util/DrawableResolver.kt b/app/src/main/java/org/schabi/newpipe/util/DrawableResolver.kt
index ccc9e7dd4af..8a728bfbfd2 100644
--- a/app/src/main/java/org/schabi/newpipe/util/DrawableResolver.kt
+++ b/app/src/main/java/org/schabi/newpipe/util/DrawableResolver.kt
@@ -4,6 +4,7 @@ import android.content.Context
import android.graphics.drawable.Drawable
import android.util.TypedValue
import androidx.annotation.AttrRes
+import androidx.core.content.ContextCompat
/**
* Utility class for resolving [Drawables](Drawable)
@@ -11,7 +12,7 @@ import androidx.annotation.AttrRes
object DrawableResolver {
@JvmStatic
fun resolveDrawable(context: Context, @AttrRes attrResId: Int): Drawable? {
- return androidx.core.content.ContextCompat.getDrawable(
+ return ContextCompat.getDrawable(
context,
TypedValue().apply {
context.theme.resolveAttribute(
From 0f551baf3729121dadc0597d615aab58ea13e941 Mon Sep 17 00:00:00 2001
From: litetex <40789489+litetex@users.noreply.github.com>
Date: Wed, 16 Mar 2022 14:53:33 +0100
Subject: [PATCH 017/240] Refactored code
---
.../helper/PlaybackParameterDialog.java | 24 +++++++++----------
.../player/helper/PlayerSemitoneHelper.java | 9 +++----
2 files changed, 17 insertions(+), 16 deletions(-)
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java
index 4ab8f9248b4..26caa1b2017 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java
@@ -44,8 +44,8 @@ public class PlaybackParameterDialog extends DialogFragment {
private static final String TAG = "PlaybackParameterDialog";
// Minimum allowable range in ExoPlayer
- private static final double MIN_PLAYBACK_VALUE = 0.10f;
- private static final double MAX_PLAYBACK_VALUE = 3.00f;
+ private static final double MIN_PITCH_OR_SPEED = 0.10f;
+ private static final double MAX_PITCH_OR_SPEED = 3.00f;
private static final boolean PITCH_CTRL_MODE_PERCENT = false;
private static final boolean PITCH_CTRL_MODE_SEMITONE = true;
@@ -62,8 +62,8 @@ public class PlaybackParameterDialog extends DialogFragment {
private static final boolean DEFAULT_SKIP_SILENCE = false;
private static final SliderStrategy QUADRATIC_STRATEGY = new SliderStrategy.Quadratic(
- MIN_PLAYBACK_VALUE,
- MAX_PLAYBACK_VALUE,
+ MIN_PITCH_OR_SPEED,
+ MAX_PITCH_OR_SPEED,
1.00f,
10_000);
@@ -177,10 +177,10 @@ public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) {
private void initUI() {
// Tempo
- setText(binding.tempoMinimumText, PlayerHelper::formatSpeed, MIN_PLAYBACK_VALUE);
- setText(binding.tempoMaximumText, PlayerHelper::formatSpeed, MAX_PLAYBACK_VALUE);
+ setText(binding.tempoMinimumText, PlayerHelper::formatSpeed, MIN_PITCH_OR_SPEED);
+ setText(binding.tempoMaximumText, PlayerHelper::formatSpeed, MAX_PITCH_OR_SPEED);
- binding.tempoSeekbar.setMax(QUADRATIC_STRATEGY.progressOf(MAX_PLAYBACK_VALUE));
+ binding.tempoSeekbar.setMax(QUADRATIC_STRATEGY.progressOf(MAX_PITCH_OR_SPEED));
setAndUpdateTempo(tempo);
binding.tempoSeekbar.setOnSeekBarChangeListener(
getTempoOrPitchSeekbarChangeListener(
@@ -215,10 +215,10 @@ private void initUI() {
changePitchControlMode(isCurrentPitchControlModeSemitone());
// Pitch - Percent
- setText(binding.pitchPercentMinimumText, PlayerHelper::formatPitch, MIN_PLAYBACK_VALUE);
- setText(binding.pitchPercentMaximumText, PlayerHelper::formatPitch, MAX_PLAYBACK_VALUE);
+ setText(binding.pitchPercentMinimumText, PlayerHelper::formatPitch, MIN_PITCH_OR_SPEED);
+ setText(binding.pitchPercentMaximumText, PlayerHelper::formatPitch, MAX_PITCH_OR_SPEED);
- binding.pitchPercentSeekbar.setMax(QUADRATIC_STRATEGY.progressOf(MAX_PLAYBACK_VALUE));
+ binding.pitchPercentSeekbar.setMax(QUADRATIC_STRATEGY.progressOf(MAX_PITCH_OR_SPEED));
setAndUpdatePitch(pitchPercent);
binding.pitchPercentSeekbar.setOnSeekBarChangeListener(
getTempoOrPitchSeekbarChangeListener(
@@ -557,12 +557,12 @@ private void setAndUpdatePitch(final double newPitch) {
}
private double calcValidTempo(final double newTempo) {
- return Math.max(MIN_PLAYBACK_VALUE, Math.min(MAX_PLAYBACK_VALUE, newTempo));
+ return Math.max(MIN_PITCH_OR_SPEED, Math.min(MAX_PITCH_OR_SPEED, newTempo));
}
private double calcValidPitch(final double newPitch) {
final double calcPitch =
- Math.max(MIN_PLAYBACK_VALUE, Math.min(MAX_PLAYBACK_VALUE, newPitch));
+ Math.max(MIN_PITCH_OR_SPEED, Math.min(MAX_PITCH_OR_SPEED, newPitch));
if (!isCurrentPitchControlModeSemitone()) {
return calcPitch;
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerSemitoneHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerSemitoneHelper.java
index abbcc2c822f..f3a71d7cd9e 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerSemitoneHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerSemitoneHelper.java
@@ -9,7 +9,7 @@
*
*/
public final class PlayerSemitoneHelper {
- public static final int TONES = 12;
+ public static final int SEMITONE_COUNT = 12;
private PlayerSemitoneHelper() {
// No impl
@@ -24,14 +24,15 @@ public static String formatPitchSemitones(final int semitones) {
}
public static double semitonesToPercent(final int semitones) {
- return Math.pow(2, ensureSemitonesInRange(semitones) / (double) TONES);
+ return Math.pow(2, ensureSemitonesInRange(semitones) / (double) SEMITONE_COUNT);
}
public static int percentToSemitones(final double percent) {
- return ensureSemitonesInRange((int) Math.round(TONES * Math.log(percent) / Math.log(2)));
+ return ensureSemitonesInRange(
+ (int) Math.round(SEMITONE_COUNT * Math.log(percent) / Math.log(2)));
}
private static int ensureSemitonesInRange(final int semitones) {
- return Math.max(-TONES, Math.min(TONES, semitones));
+ return Math.max(-SEMITONE_COUNT, Math.min(SEMITONE_COUNT, semitones));
}
}
From 1dc146322c5666f2a4af9f9d14b5f2aa56626b32 Mon Sep 17 00:00:00 2001
From: litetex <40789489+litetex@users.noreply.github.com>
Date: Thu, 17 Mar 2022 18:34:44 +0100
Subject: [PATCH 018/240] Merged ``DrawableResolver`` into ``ThemeHelper``
---
.../schabi/newpipe/local/feed/FeedFragment.kt | 2 +-
.../helper/PlaybackParameterDialog.java | 2 +-
.../schabi/newpipe/util/DrawableResolver.kt | 26 -------------------
.../org/schabi/newpipe/util/ThemeHelper.java | 18 +++++++++++++
4 files changed, 20 insertions(+), 28 deletions(-)
delete mode 100644 app/src/main/java/org/schabi/newpipe/util/DrawableResolver.kt
diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt
index 55810284f2f..b291aa03568 100644
--- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt
+++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt
@@ -75,10 +75,10 @@ import org.schabi.newpipe.local.feed.item.StreamItem
import org.schabi.newpipe.local.feed.service.FeedLoadService
import org.schabi.newpipe.local.subscription.SubscriptionManager
import org.schabi.newpipe.util.DeviceUtils
-import org.schabi.newpipe.util.DrawableResolver.resolveDrawable
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.NavigationHelper
import org.schabi.newpipe.util.ThemeHelper.getGridSpanCountStreams
+import org.schabi.newpipe.util.ThemeHelper.resolveDrawable
import org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout
import java.time.OffsetDateTime
import java.util.function.Consumer
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java
index 26caa1b2017..62446b50ec1 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java
@@ -2,8 +2,8 @@
import static org.schabi.newpipe.ktx.ViewUtils.animateRotation;
import static org.schabi.newpipe.player.Player.DEBUG;
-import static org.schabi.newpipe.util.DrawableResolver.resolveDrawable;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
+import static org.schabi.newpipe.util.ThemeHelper.resolveDrawable;
import android.app.Dialog;
import android.content.Context;
diff --git a/app/src/main/java/org/schabi/newpipe/util/DrawableResolver.kt b/app/src/main/java/org/schabi/newpipe/util/DrawableResolver.kt
deleted file mode 100644
index 8a728bfbfd2..00000000000
--- a/app/src/main/java/org/schabi/newpipe/util/DrawableResolver.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-package org.schabi.newpipe.util
-
-import android.content.Context
-import android.graphics.drawable.Drawable
-import android.util.TypedValue
-import androidx.annotation.AttrRes
-import androidx.core.content.ContextCompat
-
-/**
- * Utility class for resolving [Drawables](Drawable)
- */
-object DrawableResolver {
- @JvmStatic
- fun resolveDrawable(context: Context, @AttrRes attrResId: Int): Drawable? {
- return ContextCompat.getDrawable(
- context,
- TypedValue().apply {
- context.theme.resolveAttribute(
- attrResId,
- this,
- true
- )
- }.resourceId
- )
- }
-}
diff --git a/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java b/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java
index 7c47d387f9e..7d06e57b699 100644
--- a/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java
@@ -23,9 +23,11 @@
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
import android.util.TypedValue;
import androidx.annotation.AttrRes;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StyleRes;
import androidx.appcompat.app.ActionBar;
@@ -227,6 +229,22 @@ public static int resolveColorFromAttr(final Context context, @AttrRes final int
return value.data;
}
+ /**
+ * Resolves a {@link Drawable} by it's id.
+ *
+ * @param context Context
+ * @param attrResId Resource id
+ * @return the {@link Drawable}
+ */
+ public static Drawable resolveDrawable(
+ @NonNull final Context context,
+ @AttrRes final int attrResId
+ ) {
+ final TypedValue typedValue = new TypedValue();
+ context.getTheme().resolveAttribute(attrResId, typedValue, true);
+ return ContextCompat.getDrawable(context, typedValue.resourceId);
+ }
+
private static String getSelectedThemeKey(final Context context) {
final String themeKey = context.getString(R.string.theme_key);
final String defaultTheme = context.getResources().getString(R.string.default_theme_value);
From a311519314085d5e79e1a55afc7c16dc744cb57b Mon Sep 17 00:00:00 2001
From: litetex <40789489+litetex@users.noreply.github.com>
Date: Sat, 16 Apr 2022 21:24:01 +0200
Subject: [PATCH 019/240] Fix merge conflicts
---
.../player/helper/PlaybackParameterDialog.java | 18 ++++++------------
1 file changed, 6 insertions(+), 12 deletions(-)
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java
index 62446b50ec1..2d1461aaff0 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java
@@ -27,6 +27,7 @@
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.DialogPlaybackParameterBinding;
import org.schabi.newpipe.player.Player;
+import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener;
import org.schabi.newpipe.util.SliderStrategy;
import java.util.HashMap;
@@ -37,6 +38,8 @@
import java.util.function.DoubleFunction;
import java.util.function.DoubleSupplier;
+import javax.annotation.Nonnull;
+
import icepick.Icepick;
import icepick.State;
@@ -493,25 +496,16 @@ private SeekBar.OnSeekBarChangeListener getTempoOrPitchSeekbarChangeListener(
final SliderStrategy sliderStrategy,
final DoubleConsumer newValueConsumer
) {
- return new SeekBar.OnSeekBarChangeListener() {
+ return new SimpleOnSeekBarChangeListener() {
@Override
- public void onProgressChanged(final SeekBar seekBar, final int progress,
+ public void onProgressChanged(@Nonnull final SeekBar seekBar,
+ final int progress,
final boolean fromUser) {
if (fromUser) { // ensure that the user triggered the change
newValueConsumer.accept(sliderStrategy.valueOf(progress));
updateCallback();
}
}
-
- @Override
- public void onStartTrackingTouch(final SeekBar seekBar) {
- // Do nothing
- }
-
- @Override
- public void onStopTrackingTouch(final SeekBar seekBar) {
- // Do nothing
- }
};
}
From 8ea98b64aa81ac15f3d01e2764be38b50220644e Mon Sep 17 00:00:00 2001
From: LingYinTianMeng <2632252014@qq.com>
Date: Sun, 17 Apr 2022 22:23:03 +0800
Subject: [PATCH 020/240] fix issue #7563
---
.../local/playlist/LocalPlaylistFragment.java | 15 +++++++++++----
1 file changed, 11 insertions(+), 4 deletions(-)
diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java
index 0eb56d7169c..7cd2a3ec169 100644
--- a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java
@@ -414,14 +414,21 @@ public void removeWatchedStreams(final boolean removePartiallyWatched) {
} else {
final Iterator streamStatesIter = recordManager
.loadLocalStreamStateBatch(playlist).blockingGet().iterator();
-
while (playlistIter.hasNext()) {
final PlaylistStreamEntry playlistItem = playlistIter.next();
final int indexInHistory = Collections.binarySearch(historyStreamIds,
playlistItem.getStreamId());
-
- final boolean hasState = streamStatesIter.next() != null;
- if (indexInHistory < 0 || hasState) {
+ final StreamStateEntity streamStateEntity = streamStatesIter.next();
+ final long duration = playlistItem.toStreamInfoItem().getDuration();
+ boolean isFinished = false;
+ if (streamStateEntity != null) {
+ isFinished = streamStateEntity.isFinished(duration);
+ }
+ final boolean isNotWatchedItem = (streamStateEntity != null
+ && !isFinished);
+ if (indexInHistory < 0) {
+ notWatchedItems.add(playlistItem);
+ } else if (isNotWatchedItem) {
notWatchedItems.add(playlistItem);
} else if (!thumbnailVideoRemoved
&& playlistManager.getPlaylistThumbnail(playlistId)
From 047fe21c143fd945de34aa01c5efa4df033cb977 Mon Sep 17 00:00:00 2001
From: kt programs
Date: Sat, 30 Apr 2022 17:43:30 +0800
Subject: [PATCH 021/240] Fix hiding player controls when playing from media
button
DefaultControlDispatcher was removed in ExoPlayer 2.16.0, so the class
extending it that handled play/pause was removed in #8020.
The new solution is to use an instance of ForwardingPlayer. Call
sessionConnector.setPlayer with an instance of ForwardingPlayer that
overrides play() and pause() and calls the callback methods.
---
.../newpipe/player/helper/MediaSessionManager.java | 13 ++++++++++++-
1 file changed, 12 insertions(+), 1 deletion(-)
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java b/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java
index c12ba754ad4..a8735dc08bc 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java
@@ -13,6 +13,7 @@
import androidx.annotation.Nullable;
import androidx.media.session.MediaButtonReceiver;
+import com.google.android.exoplayer2.ForwardingPlayer;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
@@ -55,7 +56,17 @@ public MediaSessionManager(@NonNull final Context context,
sessionConnector = new MediaSessionConnector(mediaSession);
sessionConnector.setQueueNavigator(new PlayQueueNavigator(mediaSession, callback));
- sessionConnector.setPlayer(player);
+ sessionConnector.setPlayer(new ForwardingPlayer(player) {
+ @Override
+ public void play() {
+ callback.play();
+ }
+
+ @Override
+ public void pause() {
+ callback.pause();
+ }
+ });
}
@Nullable
From 173b6c3f00df02e64151ee642323205400d412f2 Mon Sep 17 00:00:00 2001
From: Stypox
Date: Sat, 30 Apr 2022 21:46:06 +0200
Subject: [PATCH 022/240] Fix wrong NonNull
---
.../schabi/newpipe/player/helper/PlaybackParameterDialog.java | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java
index 2d1461aaff0..7220335d182 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java
@@ -38,8 +38,6 @@
import java.util.function.DoubleFunction;
import java.util.function.DoubleSupplier;
-import javax.annotation.Nonnull;
-
import icepick.Icepick;
import icepick.State;
@@ -498,7 +496,7 @@ private SeekBar.OnSeekBarChangeListener getTempoOrPitchSeekbarChangeListener(
) {
return new SimpleOnSeekBarChangeListener() {
@Override
- public void onProgressChanged(@Nonnull final SeekBar seekBar,
+ public void onProgressChanged(@NonNull final SeekBar seekBar,
final int progress,
final boolean fromUser) {
if (fromUser) { // ensure that the user triggered the change
From 7e50eed95e25928cc4a40d9308ff38c216a3de0e Mon Sep 17 00:00:00 2001
From: litetex <40789489+litetex@users.noreply.github.com>
Date: Sun, 1 May 2022 20:50:37 +0200
Subject: [PATCH 023/240] Removed unused string resources
---
app/src/main/res/values-ar/strings.xml | 3 ---
app/src/main/res/values-b+zh+HANS+CN/strings.xml | 3 ---
app/src/main/res/values-ca/strings.xml | 1 -
app/src/main/res/values-ckb/strings.xml | 1 -
app/src/main/res/values-cs/strings.xml | 3 ---
app/src/main/res/values-de/strings.xml | 3 ---
app/src/main/res/values-el/strings.xml | 3 ---
app/src/main/res/values-es/strings.xml | 3 ---
app/src/main/res/values-et/strings.xml | 3 ---
app/src/main/res/values-eu/strings.xml | 3 ---
app/src/main/res/values-fa/strings.xml | 3 ---
app/src/main/res/values-fr/strings.xml | 3 ---
app/src/main/res/values-gl/strings.xml | 2 --
app/src/main/res/values-he/strings.xml | 3 ---
app/src/main/res/values-hu/strings.xml | 1 -
app/src/main/res/values-ia/strings.xml | 1 -
app/src/main/res/values-in/strings.xml | 3 ---
app/src/main/res/values-it/strings.xml | 3 ---
app/src/main/res/values-ja/strings.xml | 2 --
app/src/main/res/values-nb-rNO/strings.xml | 3 ---
app/src/main/res/values-nl/strings.xml | 3 ---
app/src/main/res/values-pl/strings.xml | 3 ---
app/src/main/res/values-pt-rBR/strings.xml | 3 ---
app/src/main/res/values-pt-rPT/strings.xml | 3 ---
app/src/main/res/values-pt/strings.xml | 4 ----
app/src/main/res/values-ro/strings.xml | 1 -
app/src/main/res/values-ru/strings.xml | 5 -----
app/src/main/res/values-sc/strings.xml | 3 ---
app/src/main/res/values-sk/strings.xml | 4 ----
app/src/main/res/values-sv/strings.xml | 3 ---
app/src/main/res/values-ta/strings.xml | 1 -
app/src/main/res/values-te/strings.xml | 1 -
app/src/main/res/values-tr/strings.xml | 3 ---
app/src/main/res/values-uk/strings.xml | 4 ----
app/src/main/res/values-vi/strings.xml | 2 --
app/src/main/res/values-zh-rHK/strings.xml | 2 --
app/src/main/res/values-zh-rTW/strings.xml | 6 ------
37 files changed, 101 deletions(-)
diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
index 3bb3048487a..e7da4611ac8 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -730,11 +730,8 @@
إظهار خطأ snackbarلم يتم العثور على مدير ملفات مناسب لهذا الإجراء.
\nالرجاء تثبيت مدير ملفات متوافق مع Storage Access Framework.
- يتم تشغيله في الخلفيةتعليق مثبتLeakCanary غير متوفر
- ضبط الصوت من خلال النغمات الموسيقية النصفية
- خطوة الإيقاعالافتراضي ExoPlayerتغيير حجم الفاصل الزمني للتحميل (حاليا %s). قد تؤدي القيمة الأقل إلى تسريع تحميل الفيديو الأولي. تتطلب التغييرات إعادة تشغيل المشغل.تكوين إشعار مشغل البث الحالي
diff --git a/app/src/main/res/values-b+zh+HANS+CN/strings.xml b/app/src/main/res/values-b+zh+HANS+CN/strings.xml
index c5b9e96462d..9170166751a 100644
--- a/app/src/main/res/values-b+zh+HANS+CN/strings.xml
+++ b/app/src/main/res/values-b+zh+HANS+CN/strings.xml
@@ -670,11 +670,8 @@
找不到适合此操作的文件管理器。
\n请安装与存储访问框架(SAF)兼容的文件管理器。NewPipe 遇到了一个错误,点击此处报告此错误
- 已经在后台播放置顶评论LeakCanary 不可用
- 以音乐半音调整音高
- 节奏步长改变加载间隔的大小(当前%s),较低的值可以加快初始的视频加载速度,改变需要重启播放器。ExoPlayer 默认配置当前正在播放的串流的通知
diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml
index 42113987505..a42e6894089 100644
--- a/app/src/main/res/values-ca/strings.xml
+++ b/app/src/main/res/values-ca/strings.xml
@@ -650,7 +650,6 @@
Inicia el reproductor principal en pantalla completaLlisqueu els elements per eliminar-losSi la rotació automàtica està bloquejada, no inicieu vídeos al mini reproductor, sinó que aneu directament al mode de pantalla completa. Podeu accedir igualment al mini reproductor sortint de pantalla completa
- Ja s\'està reproduint en segon plaNotificació d\'informe d\'errorTancar abruptament el reproductorComprovar si hi ha actualitzacions
diff --git a/app/src/main/res/values-ckb/strings.xml b/app/src/main/res/values-ckb/strings.xml
index f621b88bd15..399d2360e6c 100644
--- a/app/src/main/res/values-ckb/strings.xml
+++ b/app/src/main/res/values-ckb/strings.xml
@@ -672,7 +672,6 @@
پیشاندانی ”کڕاش کردنی لێدەرەکە“سازاندنی پەیامی کێشەیەکپشکنین بۆ نوێکردنەوە
- وا لە پاشبنەمادا لێدەدرێتکێشە لە سکاڵا کردنی پەیامپەیامەکانی سکاڵاکردن لە کێشەکانبابەتە نوێیەکانی فیید
diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml
index 84c957f1c27..9ba0d1e98ea 100644
--- a/app/src/main/res/values-cs/strings.xml
+++ b/app/src/main/res/values-cs/strings.xml
@@ -684,7 +684,6 @@
Vytvořit oznámení o chyběKontrola aktualizací…Ukázat „Shodit přehrávač“
- Hraje již v pozadíNové položky feedůPro tuto akci nebyl nalezen žádný vhodný správce souborů.
\nProsím, nainstalujte správce souborů kompatibilní se Storage Access Framework.
@@ -698,7 +697,5 @@
Shodit přehrávačZměnit interval načítání (aktuálně %s). Menší hodnota může zrychlit počáteční načítání videa. Změna vyžaduje restart přehrávače.LeakCanary není dostupné
- Upravit výšku tónů po půltónech
- Krok tempaVýchozí ExoPlayer
\ No newline at end of file
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 1b47fba2fd3..ffc501c6351 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -684,12 +684,9 @@
\nBitte installiere einen Dateimanager oder versuche, \'%s\' in den Downloadeinstellungen zu deaktivieren.Es wurde kein geeigneter Dateimanager für diese Aktion gefunden.
\nBitte installiere einen Storage Access Framework kompatiblen Dateimanager.
- Wird bereits im Hintergrund abgespieltAngehefteter KommentarLeakCanary ist nicht verfügbar
- Tonhöhe nach musikalischen Halbtönen anpassenÄndern der Größe des Ladeintervalls (derzeit %s). Ein niedrigerer Wert kann das anfängliche Laden des Videos beschleunigen. Änderungen erfordern einen Neustart des Players.
- GeschwindigkeitsstufeExoPlayer StandardBenachrichtigungenBenachrichtigen über neue abonnierbare Streams
diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml
index b54b34d609b..e202d2b34f0 100644
--- a/app/src/main/res/values-el/strings.xml
+++ b/app/src/main/res/values-el/strings.xml
@@ -682,11 +682,8 @@
\nΕγκαταστήστε έναν συμβατό με το Πλαίσιο Πρόσβασης Αποθήκευσης.Το NewPipe παρουσίασε ένα σφάλμα. Πατήστε για αναφοράΕμφάνιση μιας snackbar σφάλματος
- Αναπαράγεται ήδη στο παρασκήνιοΚαρφιτσωμένο σχόλιοΤο LeakCanary δεν είναι διαθέσιμο
- Προσαρμόστε τον τόνο με βάση τα μουσικά ημιτόνια
- Βήμα τέμποΕξ\' ορισμού ExoPlayerΑλλάξτε το μέγεθος του διαστήματος φόρτωσης (επί του παρόντος είναι %s). Μια χαμηλότερη τιμή μπορεί να επιταχύνει την αρχική φόρτωση βίντεο. Οι αλλαγές απαιτούν επανεκκίνηση της εφαρμογής.Ειδοποιήσεις
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index f386801e297..bb0decc9afa 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -686,12 +686,9 @@
No se encontró ningún gestor de archivos adecuado para esta acción.
\nPor favor instale un gestor de archivos compatible con \"Sistema de Acceso al Almacenamiento\".Comentario fijado
- Ya se reproduce en segundo planoLeakCanary no está disponibleExoPlayer valor por defecto
- Paso de tempoCambia el tamaño del intervalo de carga (actualmente %s). Un valor más bajo puede acelerar la carga inicial del vídeo. Los cambios requieren un reinicio del reproductor.
- Ajustar el tono por semitonos musicalesNotificacionesNuevos streamsNotificación del reproductor
diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml
index d04834f8a50..029ff9878d2 100644
--- a/app/src/main/res/values-et/strings.xml
+++ b/app/src/main/res/values-et/strings.xml
@@ -674,7 +674,6 @@
NewPipe töös tekkis viga, sellest teavitamiseks klõpsiJooksuta meediamängija kokkuNäita veateate akent
- Meedia esitamine taustal toimib jubaTeavitus vigadestTeavitused vigadest informeerimiseksTekkis viga, vaata vastavat teadet
@@ -709,6 +708,4 @@
Sa oled nüüd selle kanali tellija,Lülita kõik sisse
- Reguleeri helikõrgust muusikaliste pooltoonide kaupa
- Tempo samm
\ No newline at end of file
diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml
index 1c710641ebb..d4bddf4a8e3 100644
--- a/app/src/main/res/values-eu/strings.xml
+++ b/app/src/main/res/values-eu/strings.xml
@@ -666,7 +666,6 @@
Gehitu bideo hau isatsariErakutsi \"Itxi erreproduzigailua\"Prozesatzen... Itxoin mesedez
- Atzeko planoan erreproduzitzen dagoenekoErroreen txostenen jakinarazpenaJakinarazpenak erroreen berri ematekoNewPipe-k errore bat aurkitu du, sakatu berri emateko
@@ -695,8 +694,6 @@
Kanal honetara harpidetu zara,Txandakatu denak
- Doitu tonua semitono musikalen arabera
- Tempo urratsaAldatu karga maiztasun tamaina (unean %s). Balio txikiago batek bideoaren hasierako karga azkartu dezake. Erreproduzigailuaren berrabiarazte bat behar du.Harpidetzen jario berriei buruz jakinaraziEzabatu deskargatutako fitxategi guztiak biltegitik\?
diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml
index d0f4d3d841e..dac49883d88 100644
--- a/app/src/main/res/values-fa/strings.xml
+++ b/app/src/main/res/values-fa/strings.xml
@@ -683,10 +683,7 @@
نیوپایپ به خطایی برخورد. برای گزارش، بزنیدخطایی رخ داد. آگاهی را ببینیدنظر سنجاق شده
- در حال پخش در پسزمینهلیککاناری موجود نیست
- گام سرعت
- تنظیم زیر و بم با شبهتنهاتغییر اندازهٔ بازهٔ بار (هماکنون %s). مقداری پایینتر، میتواند بار کردن نخستین ویدیو را سرعت بخشد. تغییرها نیاز به یک آغاز دوبارهٔ پخشکننده دارند.پیشگزیدهٔ اگزوپلیرآگاهیها
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 4a4559477c3..1716cc06b06 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -685,11 +685,8 @@
Aucun gestionnaire de fichier approprié n\'a été trouvé pour cette action.
\nVeuillez installer un gestionnaire de fichiers ou essayez de désactiver \'%s\' dans les paramètres de téléchargement.Commentaire épinglé
- Une lecture est déjà en arrière-planLeakCanary n\'est pas disponibleModifie la taille de l\'intervalle de chargement (actuellement %s). Une valeur plus faible peut accélérer le chargement initial des vidéos .
- Règler la hauteur par demi-tons musicaux
- Pas du tempoValeur par défaut d’ExoPlayerNouveaux fluxConfigurer la notification du flux en cours de lecture
diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml
index 52225b8439b..7b49adc6d45 100644
--- a/app/src/main/res/values-gl/strings.xml
+++ b/app/src/main/res/values-gl/strings.xml
@@ -672,8 +672,6 @@
EnfileiradoProcurar actualizaciónsProcurar manualmente novas versións
- Axustar o ton do semitóns musicais
- Paso do tempoA procurar actualizacións…A partir do Android 10, só o \'Sistema de Acceso ao Almacenamento\' está soportadoCambia o tamaño do intervalo de carga (actualmente %s). Un valor menor pode acelerar o carregamento do vídeo. Cambios poden precisar un reinicio do reprodutor.
diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml
index a0e6c32a917..4d4350f077d 100644
--- a/app/src/main/res/values-he/strings.xml
+++ b/app/src/main/res/values-he/strings.xml
@@ -706,11 +706,8 @@
התראת דיווח שגיאהלא נמצאו מנהלי קבצים שמתאימים לפעולה הזאת.
\nנא להתקין מנהל קבצים שתומך בתשתית גישה לאחסון.
- כבר מתנגן ברקעהערה ננעצהLeakCanary אינה זמינה
- התאמת גובה הצליל לפי חצאי טונים מוזיקליים
- צעד מקצבברירת מחדל של ExoPlayerשינוי גודל מרווח הטעינה (כרגע %s). ערך נמוך יותר עשוי להאיץ את טעינת הווידאו הראשונית. שינויים דורשים את הפעלת הנגן מחדש.התראות על תזרימים חדשים להרשמה
diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml
index adaf6a8c028..e5457d3e104 100644
--- a/app/src/main/res/values-hu/strings.xml
+++ b/app/src/main/res/values-hu/strings.xml
@@ -683,6 +683,5 @@
%1$s letöltés törölveRögzített megjegyzés
- Már megy a lejátszás a háttérbenLeakCanary nem elérhető
\ No newline at end of file
diff --git a/app/src/main/res/values-ia/strings.xml b/app/src/main/res/values-ia/strings.xml
index e9d400be2f5..d5bc90e6f55 100644
--- a/app/src/main/res/values-ia/strings.xml
+++ b/app/src/main/res/values-ia/strings.xml
@@ -229,7 +229,6 @@
Aperir conSuggestiones de recerca remoteCargar miniaturas
- NotificationMonstrante resultatos pro: %sSolmente alicun apparatos pote reproducer videos 2K/4KInitiar le reproductor principal in schermo plen
diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml
index 056cdfce40b..1a88bb7471e 100644
--- a/app/src/main/res/values-in/strings.xml
+++ b/app/src/main/res/values-in/strings.xml
@@ -671,11 +671,8 @@
Tidak ada manajer file yang ditemukan untuk tindakan ini.
\nMohon instal sebuah manajer file yang kompatibel dengan Storage Access Framework.Komentar dipin
- Sudah diputar di latar belakangLeakCanary tidak tersedia
- Langkah tempoDefault ExoPlayer
- Atur nada berdasarkan semitone musikUbah ukuran interval pemuatan (saat ini %s). Sebuah nilai yang rendah mungkin dapat membuat pemuatan video awal lebih cepat. Membutuhkan sebuah pemulaian ulang pada pemain.Memuat detail stream…Frekuensi pemeriksaan
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index 9f7b74d8ea9..9671f861770 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -683,12 +683,9 @@
Non è stato trovato alcun gestore di file appropriato per questa azione.
\nInstallane uno compatibile con Storage Access Framework.Commento in primo piano
- Già in riproduzione in sottofondoLeakCanary non è disponibile
- Regola il tono secondo i semitoni musicaliPredefinito ExoPlayerCambia la dimensione dell\'intervallo da caricare (attualmente %s). Un valore basso può velocizzare il caricamento iniziale del video. La modifica richiede il riavvio del lettore.
- Passo tempoNotifiche di nuove stream dalle iscrizioniFrequenza controlloRichiesta connessione alla rete
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index a0699f22907..9a15b25d121 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -663,7 +663,6 @@
エラーが発生しました。通知をご覧くださいNewPipe はエラーに遭遇しました。タップして報告スナックバーにエラーを表示
- 既にバックグラウンドで再生されています固定されたコメントこの動作に適切なファイルマネージャが見つかりませんでした。
\nStorage Access Frameworkと互換性のあるファイルマネージャをインストールしてください。
@@ -673,7 +672,6 @@
エラー通知を作成エラーを報告する通知LeakCanaryが利用不可能です
- 緩急音階プレイヤー通知ストリームの詳細を読み込んでいます…登録チャンネルの新しいストリームについて通知する
diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml
index 92b24fbc80a..b2829ba27c7 100644
--- a/app/src/main/res/values-nb-rNO/strings.xml
+++ b/app/src/main/res/values-nb-rNO/strings.xml
@@ -672,7 +672,6 @@
Viser et krasjalternativ ved bruk av avspillerenDet oppstod en feil. Sjekk merknaden.Festet kommentar
- Spilles allerede i bakgrunnenFeilrapport-merknadMerknader for innrapportering av feilNewPipe-feil. Trykk for å rapportere.
@@ -682,6 +681,4 @@
Installer en filbehandler som støtter lagringstilgangsrammeverk først.LeakCanary er ikke tilgjengeligExoPlayer-forvalg
- Juster toneart etter musikalske halvtoner
- Tempo-steg
\ No newline at end of file
diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml
index dd405963ae0..3a943d2cad1 100644
--- a/app/src/main/res/values-nl/strings.xml
+++ b/app/src/main/res/values-nl/strings.xml
@@ -677,7 +677,6 @@
NewPipe meldt fout, tik voor berichtFoutmeldingMaak een foutmelding
- Speelt al op de achtergrondKorte foutmelding weergevenEr is geen geschikte bestandsbeheerder gevonden voor deze actie.
\nInstalleer een bestandsbeheerder of probeer \'%s\' uit te schakelen in de download instellingen.
@@ -686,7 +685,5 @@
Vastgemaakt commentaarLeakCanary is niet beschikbaarVerander de laad interval tijd (nu %s). Een lagere waarde kan het initiële laden van de video versnellen. De wijziging vereist een herstart van de speler.
- Pas de toonhoogte aan met muzikale halve tonen
- Tempo stapExoPlayer standaard
\ No newline at end of file
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index cfa995212a2..6e2c95119a8 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -701,14 +701,11 @@
\nZainstaluj menedżer plików lub spróbuj wyłączyć „%s” w ustawieniach pobierania.Nie znaleziono odpowiedniego menedżera plików dla tej akcji.
\nZainstaluj menedżer plików zgodny z Storage Access Framework.
- Już jest odtwarzane w tlePrzypięty komentarzLeakCanary jest niedostępneRozmiar interwału ładowania odtwarzaniaZmień rozmiar interwału ładowania (aktualnie %s). Niższa wartość może przyspieszyć początkowe ładowanie wideo. Zmiany wymagają ponownego uruchomienia odtwarzaczadomyślny ExoPlayera
- Dostosuj wysokość półtonami
- Krok tempaPowiadomienie odtwarzaczaSkonfiguruj powiadomienie aktualnie odtwarzanego strumieniaUruchom sprawdzenie nowych strumieni
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index 0850034f66e..d19196c0644 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -682,12 +682,9 @@
Mostrar um snackbar de erroNenhum gerenciador de arquivos apropriado foi encontrado para esta ação.
\nInstale um gerenciador de arquivos ou tente desativar \'%s\' nas configurações de download.
- Já está tocando em segundo planoComentário fixadoO LeakCanary não está disponível
- Passo do tempoAltere o tamanho do intervalo de carregamento (atualmente %s). Um valor menor pode acelerar o carregamento inicial do vídeo. As alterações exigem que o player reinicie.
- Ajustar o tom por semitons musicaisExoPlayer padrãoNotificação do reprodutorConfigurar a notificação do fluxo da reprodução atual
diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml
index 43e78225148..aa3862b6be0 100644
--- a/app/src/main/res/values-pt-rPT/strings.xml
+++ b/app/src/main/res/values-pt-rPT/strings.xml
@@ -683,11 +683,8 @@
Nenhum gestor de ficheiros apropriado foi encontrado para esta ação.
\nPor favor, instale um gestor de ficheiros compatível com o Storage Access Framework.Comentário fixado
- Já está a reproduzir em segundo planoLeakCanary não está disponívelAltere o tamanho do intervalo de carregamento (atualmente %s). Um valor menor pode acelerar o carregamento inicial do vídeo. Se fizer alterações é necessário reiniciar.
- Ajustar o tom por semitons musicais
- Passo do tempoPredefinido do ExoPlayerNotificaçõesA carregar detalhes do fluxo…
diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml
index 9e1a8091621..9563c9a6fe9 100644
--- a/app/src/main/res/values-pt/strings.xml
+++ b/app/src/main/res/values-pt/strings.xml
@@ -554,7 +554,6 @@
URL não reconhecido. Abrir com outra aplicação\?Enfileiramento automáticoEmbaralhar
- NotificaçãoApenas em Wi-FiNadaMudar de um reprodutor para outro pode substituir a sua fila
@@ -683,13 +682,10 @@
\nPor favor, instale um gestor de ficheiros ou tente desativar \'%s\' nas configurações de descarregar.Nenhum gestor de ficheiros apropriado foi encontrado para esta ação.
\nPor favor, instale um gestor de ficheiros compatível com o Storage Access Framework.
- Já está a reproduzir em segundo planoComentário fixadoLeakCanary não está disponível
- Ajustar o tom por semitons musicaisPredefinido do ExoPlayerAltere o tamanho do intervalo de carregamento (atualmente %s). Um valor menor pode acelerar o carregamento inicial do vídeo. Se fizer alterações é necessário reiniciar.
- Passo do tempoNotificação do reprodutorConfigurar a notificação da reprodução do fluxo atualNotificações
diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml
index e83fc5462cd..67108b500cb 100644
--- a/app/src/main/res/values-ro/strings.xml
+++ b/app/src/main/res/values-ro/strings.xml
@@ -679,7 +679,6 @@
Procesarea.. Poate dura un momentVerifică dacă există actualizăriVerifică manual dacă există versiuni noi
- Se redă deja pe fundalComentariu lipitNotificare cu raport de eroareAfișează opțiunea de a întrerupe atunci când utilizați playerul
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index fc4c03f42c7..2a16a45731f 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -691,8 +691,6 @@
Проверить обновленияПроверка обновлений…Новое на канале
- Отчёт об ошибках плеера
- Подробные отчёты об ошибках плеера вместо коротких всплывающих сообщений (полезно при диагностике проблем)УведомленияНовые видеоУведомления о новых видео в подписках
@@ -718,11 +716,8 @@
\nПожалуйста, установите файловый менеджер, или попробуйте отключить \'%s\' в настройках загрузок.
Для этого действия не найдено подходящего файлового менеджера.
\nПожалуйста, установите файловый менеджер, совместимый со Storage Access Framework (SAF).
- Уже проигрывается в фонеЗакреплённый комментарийLeakCanary недоступна
- Регулировка высоты тона по музыкальным полутонам
- Шаг темпаСтандартное значение ExoPlayerИзменить размер интервала загрузки (сейчас %s). Меньшее значение может ускорить начальную загрузку видео. Изменение значения потребует перезапуска плеера.Загрузка деталей трансляции…
diff --git a/app/src/main/res/values-sc/strings.xml b/app/src/main/res/values-sc/strings.xml
index 38a7c330312..7c6120832b6 100644
--- a/app/src/main/res/values-sc/strings.xml
+++ b/app/src/main/res/values-sc/strings.xml
@@ -683,10 +683,7 @@
\nPro praghere installa unu gestore de documentos cumpatìbile cun su \"Sistema de Atzessu a s\'Archiviatzione\".
Faghe serrare su riproduidoreCummentu apicadu
- Giai in riprodutzione in s\'isfunduLeakCanary no est a disponimentu
- Règula s\'intonatzione in base a sos semitonos musicales
- Passu de tempusValore ExoPlayer predefiniduMuda sa mannària de s\'intervallu de carrigamentu (in custu momentu %s). Unu valore prus bassu diat pòdere allestrare su carrigamentu de incumintzu de su vìdeu. Sas modìficas tenent bisòngiu de torrare a allùghere su riproduidore.Cunfigura sa notìfica de su flussu in cursu de riprodutzione
diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml
index 9b7a621450a..c47562bb00e 100644
--- a/app/src/main/res/values-sk/strings.xml
+++ b/app/src/main/res/values-sk/strings.xml
@@ -647,8 +647,6 @@
Pri každom sťahovaní sa zobrazí výzva kam uložiť súborNie je nastavený adresár na sťahovanie, nastavte ho terazOznačiť ako videné
- Načítavanie podrobností o kanáli…
- Chyba pri zobrazení podrobností kanálaVypnutéZapnutéRežim tabletu
@@ -698,8 +696,6 @@
Zobrazí možnosť zlyhania pri používaní prehrávačaZobraziť krátke oznámenie chybyOznámte chybu
- Upraviť výšku poltónov
- Krok tempaExoPlayer preddefinovanýZmeniť interval načítania (aktuálne %s). Menšia hodnota môže zvýšiť rýchlosť prvotného načítania videa. Zmena vyžaduje reštart.Upozornenia
diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml
index 63af6d4d613..b7e66a85baa 100644
--- a/app/src/main/res/values-sv/strings.xml
+++ b/app/src/main/res/values-sv/strings.xml
@@ -682,13 +682,10 @@
\nInstallera en filhanterare eller testa att inaktivera \'%s\' i nedladdningsinställningarna.
Ingen lämplig filhanterare hittades för denna åtgärd.
\nInstallera en filhanterare som är kompatibel med Storage Access Framework.
- Spelas redan i bakgrundenFäst kommentarLeakCanary är inte tillgänglig
- Justera tonhöjden med musikaliska halvtonerExoPlayer standardÄndra inläsningsintervallets storlek (för närvarande %s). Ett lägre värde kan påskynda den första videoinläsningen. Ändringar kräver omstart av spelaren.
- TempostegValidera frekvensKräver nätverksanslutningAlla nätverk
diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml
index d7c920f2ba1..827da452a6f 100644
--- a/app/src/main/res/values-ta/strings.xml
+++ b/app/src/main/res/values-ta/strings.xml
@@ -253,7 +253,6 @@
இயக்குதலைத் மறுதொடர்நிநிகழ்வு ஏற்கனவே உள்ளது
- அறிவிப்புயூடியூபின் \"கட்டுப்பாடு பயன்முறை\"ஐ இயக்குபாடல்கள்பிழைகளைப் புகாரளிக்க அறிவிப்புகள்
diff --git a/app/src/main/res/values-te/strings.xml b/app/src/main/res/values-te/strings.xml
index 60542e0b6d4..3958e02b005 100644
--- a/app/src/main/res/values-te/strings.xml
+++ b/app/src/main/res/values-te/strings.xml
@@ -371,7 +371,6 @@
reCAPTCHA సవాలుreCAPTCHA సవాలు అభ్యర్థించబడిందిప్లేజాబితాను ఎంచుకోండి
- ఇప్పటికే వెనుకగా ప్లే అవుతోందిడాటాబేసుని ఎగుమతిచేయుముయాప్ పునఃప్రారంభించబడిన తర్వాత భాష మారుతుందిఛానెల్ వివరాలను చూపు
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index 958dc5eeb0d..a9bbe30e586 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -682,13 +682,10 @@
Hata raporları için bildirimlerOynatıcı kullanırken çöktürme seçeneği gösterirOynatıcıyı çöktür
- Zaten arka planda oynuyorSabitlenmiş yorumLeakCanary yokYükleme ara boyutunu değiştir (şu anda %s). Düşük bir değer videonun ilk yüklenişini hızlandırabilir. Değişiklikler oynatıcının yeniden başlatılmasını gerektirir.ExoPlayer öntanımlısı
- Tempo adımı
- Perdeyi müzikal yarım tonlarla uyarlaYeni akış bildirimleriBildirimler
diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml
index f2a3e986e15..0768f241b44 100644
--- a/app/src/main/res/values-uk/strings.xml
+++ b/app/src/main/res/values-uk/strings.xml
@@ -58,7 +58,6 @@
ЗавантаженняЗвіт про помилкуУсе
- ТакВимкненоЗбій застосунку/інтерфейсуВаш коментар (англійською):
@@ -699,11 +698,8 @@
\nУстановіть файловий менеджер, сумісний зі Storage Access Framework.
Показати панель помилокСтворити сповіщення про помилку
- Уже відтворюється у фоновому режиміЗакріплений коментарLeakCanary недоступний
- Крок темпу
- Регулювання висоти звуку за музичними півтонамиТиповий ExoPlayerЗмінити розмір інтервалу завантаження (наразі %s). Менше значення може прискорити початкове завантаження відео. Зміни вимагають перезапуску програвача.Ви підписалися на цей канал
diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml
index 7ea1a1ed5ee..08bf7cae08b 100644
--- a/app/src/main/res/values-vi/strings.xml
+++ b/app/src/main/res/values-vi/strings.xml
@@ -672,8 +672,6 @@
\nVui lòng cài đặt ứng dụng quản lý tệp hoặc tắt \'%s\' trong cài đặt tải xuống.
Thay đổi kích thước khoảng thời gian tải (tầm khoảng %s). Để ở giá trị thấp hơn có thể sẽ tăng tốc độ tải video hơn ban đầu. Khởi động lại trình phát để áp dụng thay đổi.LeakCanary không khả dụng
- Điều chỉnh cao độ theo nhạc nền âm nhạc
- Nhịp độ tiếp theoExoPlayer mặc địnhBình luận được ghim
\ No newline at end of file
diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml
index 0e79f5a5833..d527edc9b34 100644
--- a/app/src/main/res/values-zh-rHK/strings.xml
+++ b/app/src/main/res/values-zh-rHK/strings.xml
@@ -603,10 +603,8 @@
%s 個新加串流
- 按樂音半度調整音高新加串流通知係咪要喺磁碟機上面消除晒全部下載咗嘅檔案?通知已停用單曲
- 節奏步伐
\ No newline at end of file
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index fd129fb6c76..8b5a246611d 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -539,7 +539,6 @@
作用中播放器的佇列可能會被取代從一個播放器切換到另一個可能會取代您的佇列清除佇列前要求確認
- 通知無正在緩衝隨機播放
@@ -638,8 +637,6 @@
拖動列縮圖預覽被創作者加心號標記為已看過
- 正在載入頻道詳細資訊……
- 顯示頻道詳細資訊時發生錯誤在圖片頂部顯示畢卡索彩色絲帶,指示其來源:紅色代表網路、藍色代表磁碟、綠色代表記憶體顯示圖片指示器遠端搜尋建議
@@ -674,12 +671,9 @@
NewPipe 遇到錯誤,點擊以回報發生錯誤,請檢視通知釘選的留言
- 已經在背景播放LeakCanary 無法使用
- 步進時間ExoPlayer 預設值變更載入間隔大小(目前為 %s)。較低的值可能會提昇初始影片載入速度。變更需要重新啟動播放器。
- 按音樂半音調整音高播放器通知通知正在載入串流詳細資訊……
From a67927c29ce5e3f60ed628fb307bda3333e28774 Mon Sep 17 00:00:00 2001
From: litetex <40789489+litetex@users.noreply.github.com>
Date: Sun, 1 May 2022 21:56:49 +0200
Subject: [PATCH 024/240] Fix dialogs having incorrect color when opened via
RouterActivity
---
app/src/main/res/values/styles.xml | 8 --------
1 file changed, 8 deletions(-)
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 7c126558079..894e70bad92 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -131,10 +131,6 @@
+
-
-
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 894e70bad92..e711b35ab2c 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -130,7 +130,8 @@
@color/black_settings_accent_color
-
-
+
+
From 24cf19710fae23a1a1594c2dc7ec2fadb2796ff2 Mon Sep 17 00:00:00 2001
From: TacoTheDank
Date: Thu, 9 Jun 2022 11:34:57 -0400
Subject: [PATCH 065/240] Clean up proguard file
---
app/proguard-rules.pro | 11 +++--------
1 file changed, 3 insertions(+), 8 deletions(-)
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index 4a54d8992e9..5e10d3916ab 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -18,7 +18,6 @@
-dontobfuscate
-keep class org.schabi.newpipe.extractor.timeago.patterns.** { *; }
--keep class org.ocpsoft.prettytime.i18n.** { *; }
-keep class org.mozilla.javascript.** { *; }
@@ -26,9 +25,6 @@
-keep class com.google.android.exoplayer2.** { *; }
-dontwarn org.mozilla.javascript.tools.**
--dontwarn android.arch.util.paging.CountedDataSource
--dontwarn android.arch.persistence.room.paging.LimitOffsetDataSource
-
# Rules for icepick. Copy paste from https://github.com/frankiesardo/icepick
-dontwarn icepick.**
@@ -39,12 +35,11 @@
}
-keepnames class * { @icepick.State *;}
-# Rules for OkHttp. Copy paste from https://github.com/square/okhttp
+## Rules for OkHttp. Copy paste from https://github.com/square/okhttp
-dontwarn okhttp3.**
-dontwarn okio.**
--dontwarn javax.annotation.**
-# A resource is loaded with a relative path so the package of this class must be preserved.
--keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase
+##
+
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
!static !transient ;
From 210834fbe933654804016e547833288f90e2135e Mon Sep 17 00:00:00 2001
From: AudricV <74829229+AudricV@users.noreply.github.com>
Date: Thu, 16 Jun 2022 11:13:19 +0200
Subject: [PATCH 066/240] Add support of other delivery methods than
progressive HTTP (in the player only)
Detailed changes:
- External players:
- Add a message instruction about stream selection;
- Add a message when there is no stream available for external players;
- Return now HLS, DASH and SmoothStreaming URL contents, in addition to progressive HTTP ones.
- Player:
- Support DASH, HLS and SmoothStreaming streams for videos, whether they are content URLs or the manifests themselves, in addition to progressive HTTP ones;
- Use a custom HttpDataSource to play YouTube contents, based of ExoPlayer's default one, which allows better spoofing of official clients (custom user-agent and headers (depending of the client used), use of range and rn (set dynamically by the DataSource) parameters);
- Fetch YouTube progressive contents as DASH streams, like official clients, support fully playback of livestreams which have ended recently and OTF streams;
- Use ExoPlayer's default retries count for contents on non-fatal errors (instead of Integer.MAX_VALUE for non-live contents and 5 for live contents).
- Download dialog:
- Add message about support of progressive HTTP streams only for downloading;
- Remove several duplicated code and update relevant usages;
- Support downloading of contents with an unknown media format.
- ListHelper:
- Catch NumberFormatException when trying to compare two video streams between them.
- Tests:
- Update ListHelperTest and StreamItemAdapterTest to fix breaking changes in the extractor.
- Other places:
- Fixes deprecation of changes made in the extractor;
- Improve some code related to the files changed.
- Issues fixed and/or improved with the changes:
- Seeking of PeerTube HLS streams (the duration shown was the one from the stream duration and not the one parsed, incomplete because HLS streams are fragmented MP4s with multiple sidx boxes, for which seeking is not supported by ExoPlayer) (the app now uses the HLS manifest returned for each quality, in the master playlist (not fetched and computed by the extractor));
- Crash when loading PeerTube streams with a separated audio;
- Lack of some streams on some YouTube videos (OTF streams);
- Loading times of YouTube streams, after a quality change or a playback start;
- View count of YouTube ended livestreams interpreted as watching count (this type of streams is not interpreted anymore as livestreams);
- Watchable time of YouTube ended livestreams;
- Playback of SoundCloud HLS-only tracks (which cannot be downloaded anymore because the workaround which was used is being removed by SoundCloud, so it has been removed from the extractor).
---
app/build.gradle | 2 +-
.../newpipe/util/StreamItemAdapterTest.kt | 36 +-
.../org/schabi/newpipe/RouterActivity.java | 18 +-
.../newpipe/download/DownloadDialog.java | 261 +++--
.../fragments/detail/VideoDetailFragment.java | 82 +-
.../holder/StreamMiniInfoItemHolder.java | 6 +-
.../newpipe/local/feed/item/StreamItem.kt | 4 +-
.../org/schabi/newpipe/player/Player.java | 55 +-
.../datasource/YoutubeHttpDataSource.java | 1031 +++++++++++++++++
.../newpipe/player/helper/CacheFactory.java | 116 +-
.../NonUriHlsPlaylistParserFactory.java | 50 +
.../player/helper/PlayerDataSource.java | 127 +-
.../newpipe/player/helper/PlayerHelper.java | 67 +-
.../listeners/view/QualityClickListener.kt | 2 +-
.../resolver/AudioPlaybackResolver.java | 27 +-
.../player/resolver/PlaybackResolver.java | 407 ++++++-
.../resolver/VideoPlaybackResolver.java | 89 +-
.../org/schabi/newpipe/util/ListHelper.java | 130 ++-
.../schabi/newpipe/util/NavigationHelper.java | 85 +-
.../newpipe/util/SecondaryStreamHelper.java | 48 +-
.../newpipe/util/StreamItemAdapter.java | 46 +-
.../schabi/newpipe/util/StreamTypeUtil.java | 8 +-
.../giga/get/DownloadMissionRecover.java | 28 +-
.../shandian/giga/get/MissionRecoveryInfo.kt | 12 +-
app/src/main/res/layout/download_dialog.xml | 13 +
app/src/main/res/values/strings.xml | 7 +
.../schabi/newpipe/util/ListHelperTest.java | 187 +--
27 files changed, 2411 insertions(+), 533 deletions(-)
create mode 100644 app/src/main/java/org/schabi/newpipe/player/datasource/YoutubeHttpDataSource.java
create mode 100644 app/src/main/java/org/schabi/newpipe/player/helper/NonUriHlsPlaylistParserFactory.java
diff --git a/app/build.gradle b/app/build.gradle
index 44fd7512b50..995dae6ed5f 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -190,7 +190,7 @@ dependencies {
// name and the commit hash with the commit hash of the (pushed) commit you want to test
// This works thanks to JitPack: https://jitpack.io/
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
- implementation 'com.github.TeamNewPipe:NewPipeExtractor:ac1c22d81c65b7b0c5427f4e1989f5256d617f32'
+ implementation 'com.github.TeamNewPipe:NewPipeExtractor:1b51eab664ec7cbd2295c96d8b43000379cd1b7b'
/** Checkstyle **/
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
diff --git a/app/src/androidTest/java/org/schabi/newpipe/util/StreamItemAdapterTest.kt b/app/src/androidTest/java/org/schabi/newpipe/util/StreamItemAdapterTest.kt
index a9aa40d8273..016feb57645 100644
--- a/app/src/androidTest/java/org/schabi/newpipe/util/StreamItemAdapterTest.kt
+++ b/app/src/androidTest/java/org/schabi/newpipe/util/StreamItemAdapterTest.kt
@@ -91,7 +91,12 @@ class StreamItemAdapterTest {
context,
StreamItemAdapter.StreamSizeWrapper(
(0 until 5).map {
- SubtitlesStream(MediaFormat.SRT, "pt-BR", "https://example.com", false)
+ SubtitlesStream.Builder()
+ .setContent("https://example.com", true)
+ .setMediaFormat(MediaFormat.SRT)
+ .setLanguageCode("pt-BR")
+ .setAutoGenerated(false)
+ .build()
},
context
),
@@ -108,7 +113,14 @@ class StreamItemAdapterTest {
val adapter = StreamItemAdapter(
context,
StreamItemAdapter.StreamSizeWrapper(
- (0 until 5).map { AudioStream("https://example.com/$it", MediaFormat.OPUS, 192) },
+ (0 until 5).map {
+ AudioStream.Builder()
+ .setId(Stream.ID_UNKNOWN)
+ .setContent("https://example.com/$it", true)
+ .setMediaFormat(MediaFormat.OPUS)
+ .setAverageBitrate(192)
+ .build()
+ },
context
),
null
@@ -126,7 +138,13 @@ class StreamItemAdapterTest {
private fun getVideoStreams(vararg videoOnly: Boolean) =
StreamItemAdapter.StreamSizeWrapper(
videoOnly.map {
- VideoStream("https://example.com", MediaFormat.MPEG_4, "720p", it)
+ VideoStream.Builder()
+ .setId(Stream.ID_UNKNOWN)
+ .setContent("https://example.com", true)
+ .setMediaFormat(MediaFormat.MPEG_4)
+ .setResolution("720p")
+ .setIsVideoOnly(it)
+ .build()
},
context
)
@@ -138,8 +156,16 @@ class StreamItemAdapterTest {
private fun getAudioStreams(vararg shouldBeValid: Boolean) =
getSecondaryStreamsFromList(
shouldBeValid.map {
- if (it) AudioStream("https://example.com", MediaFormat.OPUS, 192)
- else null
+ if (it) {
+ AudioStream.Builder()
+ .setId(Stream.ID_UNKNOWN)
+ .setContent("https://example.com", true)
+ .setMediaFormat(MediaFormat.OPUS)
+ .setAverageBitrate(192)
+ .build()
+ } else {
+ null
+ }
}
)
diff --git a/app/src/main/java/org/schabi/newpipe/RouterActivity.java b/app/src/main/java/org/schabi/newpipe/RouterActivity.java
index cc89c0fed61..96f8ff1bceb 100644
--- a/app/src/main/java/org/schabi/newpipe/RouterActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/RouterActivity.java
@@ -58,7 +58,6 @@
import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.stream.StreamInfo;
-import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.player.MainPlayer;
@@ -677,22 +676,15 @@ private void openDownloadDialog() {
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {
- final List sortedVideoStreams = ListHelper
- .getSortedStreamVideosList(this, result.getVideoStreams(),
- result.getVideoOnlyStreams(), false, false);
- final int selectedVideoStreamIndex = ListHelper
- .getDefaultResolutionIndex(this, sortedVideoStreams);
+ final DownloadDialog downloadDialog = DownloadDialog.newInstance(this, result);
+ downloadDialog.setSelectedVideoStream(ListHelper.getDefaultResolutionIndex(
+ this, downloadDialog.wrappedVideoStreams.getStreamsList()));
+ downloadDialog.setOnDismissListener(dialog -> finish());
final FragmentManager fm = getSupportFragmentManager();
- final DownloadDialog downloadDialog = DownloadDialog.newInstance(result);
- downloadDialog.setVideoStreams(sortedVideoStreams);
- downloadDialog.setAudioStreams(result.getAudioStreams());
- downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex);
- downloadDialog.setOnDismissListener(dialog -> finish());
downloadDialog.show(fm, "downloadDialog");
fm.executePendingTransactions();
- }, throwable ->
- showUnsupportedUrlDialog(currentUrl)));
+ }, throwable -> showUnsupportedUrlDialog(currentUrl)));
}
@Override
diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java
index f5c22690836..73ba8c74a7b 100644
--- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java
+++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java
@@ -48,6 +48,7 @@
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.localization.Localization;
import org.schabi.newpipe.extractor.stream.AudioStream;
+import org.schabi.newpipe.extractor.stream.DeliveryMethod;
import org.schabi.newpipe.extractor.stream.Stream;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
@@ -71,6 +72,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
+import java.util.Objects;
import icepick.Icepick;
import icepick.State;
@@ -82,6 +84,7 @@
import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder;
import us.shandian.giga.service.MissionState;
+import static org.schabi.newpipe.util.ListHelper.keepStreamsWithDelivery;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
public class DownloadDialog extends DialogFragment
@@ -92,11 +95,11 @@ public class DownloadDialog extends DialogFragment
@State
StreamInfo currentInfo;
@State
- StreamSizeWrapper wrappedAudioStreams = StreamSizeWrapper.empty();
+ public StreamSizeWrapper wrappedAudioStreams = StreamSizeWrapper.empty();
@State
- StreamSizeWrapper wrappedVideoStreams = StreamSizeWrapper.empty();
+ public StreamSizeWrapper wrappedVideoStreams = StreamSizeWrapper.empty();
@State
- StreamSizeWrapper wrappedSubtitleStreams = StreamSizeWrapper.empty();
+ public StreamSizeWrapper wrappedSubtitleStreams = StreamSizeWrapper.empty();
@State
int selectedVideoIndex = 0;
@State
@@ -138,28 +141,39 @@ public class DownloadDialog extends DialogFragment
registerForActivityResult(
new StartActivityForResult(), this::requestDownloadPickVideoFolderResult);
-
/*//////////////////////////////////////////////////////////////////////////
// Instance creation
//////////////////////////////////////////////////////////////////////////*/
- public static DownloadDialog newInstance(final StreamInfo info) {
- final DownloadDialog dialog = new DownloadDialog();
- dialog.setInfo(info);
- return dialog;
- }
+ @NonNull
+ public static DownloadDialog newInstance(final Context context,
+ @NonNull final StreamInfo info) {
+ // TODO: Adapt this code when the downloader support other types of stream deliveries
+ final List videoStreams = new ArrayList<>(info.getVideoStreams());
+ final List progressiveHttpVideoStreams =
+ keepStreamsWithDelivery(videoStreams, DeliveryMethod.PROGRESSIVE_HTTP);
+
+ final List videoOnlyStreams = new ArrayList<>(info.getVideoOnlyStreams());
+ final List progressiveHttpVideoOnlyStreams =
+ keepStreamsWithDelivery(videoOnlyStreams, DeliveryMethod.PROGRESSIVE_HTTP);
+
+ final List audioStreams = new ArrayList<>(info.getAudioStreams());
+ final List progressiveHttpAudioStreams =
+ keepStreamsWithDelivery(audioStreams, DeliveryMethod.PROGRESSIVE_HTTP);
+
+ final List subtitlesStreams = new ArrayList<>(info.getSubtitles());
+ final List progressiveHttpSubtitlesStreams =
+ keepStreamsWithDelivery(subtitlesStreams, DeliveryMethod.PROGRESSIVE_HTTP);
- public static DownloadDialog newInstance(final Context context, final StreamInfo info) {
- final ArrayList streamsList = new ArrayList<>(ListHelper
- .getSortedStreamVideosList(context, info.getVideoStreams(),
- info.getVideoOnlyStreams(), false, false));
- final int selectedStreamIndex = ListHelper.getDefaultResolutionIndex(context, streamsList);
+ final List videoStreamsList = new ArrayList<>(
+ ListHelper.getSortedStreamVideosList(context, progressiveHttpVideoStreams,
+ progressiveHttpVideoOnlyStreams, false, false));
- final DownloadDialog instance = newInstance(info);
- instance.setVideoStreams(streamsList);
- instance.setSelectedVideoStream(selectedStreamIndex);
- instance.setAudioStreams(info.getAudioStreams());
- instance.setSubtitleStreams(info.getSubtitles());
+ final DownloadDialog instance = new DownloadDialog();
+ instance.setInfo(info);
+ instance.setVideoStreams(videoStreamsList);
+ instance.setAudioStreams(progressiveHttpAudioStreams);
+ instance.setSubtitleStreams(progressiveHttpSubtitlesStreams);
return instance;
}
@@ -169,45 +183,69 @@ public static DownloadDialog newInstance(final Context context, final StreamInfo
// Setters
//////////////////////////////////////////////////////////////////////////*/
- private void setInfo(final StreamInfo info) {
+ private void setInfo(@NonNull final StreamInfo info) {
this.currentInfo = info;
}
- public void setAudioStreams(final List audioStreams) {
- setAudioStreams(new StreamSizeWrapper<>(audioStreams, getContext()));
- }
-
- public void setAudioStreams(final StreamSizeWrapper was) {
- this.wrappedAudioStreams = was;
- }
-
- public void setVideoStreams(final List videoStreams) {
- setVideoStreams(new StreamSizeWrapper<>(videoStreams, getContext()));
+ public void setAudioStreams(@NonNull final List audioStreams) {
+ this.wrappedAudioStreams = new StreamSizeWrapper<>(audioStreams, getContext());
}
- public void setVideoStreams(final StreamSizeWrapper wvs) {
- this.wrappedVideoStreams = wvs;
+ public void setVideoStreams(@NonNull final List videoStreams) {
+ this.wrappedVideoStreams = new StreamSizeWrapper<>(videoStreams, getContext());
}
- public void setSubtitleStreams(final List subtitleStreams) {
- setSubtitleStreams(new StreamSizeWrapper<>(subtitleStreams, getContext()));
- }
-
- public void setSubtitleStreams(
- final StreamSizeWrapper wss) {
- this.wrappedSubtitleStreams = wss;
+ public void setSubtitleStreams(@NonNull final List subtitleStreams) {
+ this.wrappedSubtitleStreams = new StreamSizeWrapper<>(subtitleStreams, getContext());
}
+ /**
+ * Set the selected video stream, by using its index in the stream list.
+ *
+ * The index of the select video stream will be not set if this index is not in the bounds
+ * of the stream list.
+ *
+ * @param svi the index of the selected {@link VideoStream}
+ */
public void setSelectedVideoStream(final int svi) {
- this.selectedVideoIndex = svi;
+ if (selectedStreamIsInBoundsOfWrappedStreams(svi, this.wrappedVideoStreams)) {
+ this.selectedVideoIndex = svi;
+ }
}
+ /**
+ * Set the selected audio stream, by using its index in the stream list.
+ *
+ * The index of the select audio stream will be not set if this index is not in the bounds
+ * of the stream list.
+ *
+ * @param sai the index of the selected {@link AudioStream}
+ */
public void setSelectedAudioStream(final int sai) {
- this.selectedAudioIndex = sai;
+ if (selectedStreamIsInBoundsOfWrappedStreams(sai, this.wrappedAudioStreams)) {
+ this.selectedAudioIndex = sai;
+ }
}
+ /**
+ * Set the selected subtitles stream, by using its index in the stream list.
+ *
+ * The index of the select subtitles stream will be not set if this index is not in the bounds
+ * of the stream list.
+ *
+ * @param ssi the index of the selected {@link SubtitlesStream}
+ */
public void setSelectedSubtitleStream(final int ssi) {
- this.selectedSubtitleIndex = ssi;
+ if (selectedStreamIsInBoundsOfWrappedStreams(ssi, this.wrappedSubtitleStreams)) {
+ this.selectedSubtitleIndex = ssi;
+ }
+ }
+
+ private boolean selectedStreamIsInBoundsOfWrappedStreams(
+ final int selectedIndexStream,
+ final StreamSizeWrapper extends Stream> wrappedStreams) {
+ return selectedIndexStream > 0
+ && selectedIndexStream < wrappedStreams.getStreamsList().size();
}
public void setOnDismissListener(@Nullable final OnDismissListener onDismissListener) {
@@ -249,11 +287,16 @@ public void onCreate(@Nullable final Bundle savedInstanceState) {
.getAudioStreamFor(wrappedAudioStreams.getStreamsList(), videoStreams.get(i));
if (audioStream != null) {
- secondaryStreams
- .append(i, new SecondaryStreamHelper<>(wrappedAudioStreams, audioStream));
+ secondaryStreams.append(i, new SecondaryStreamHelper<>(wrappedAudioStreams,
+ audioStream));
} else if (DEBUG) {
- Log.w(TAG, "No audio stream candidates for video format "
- + videoStreams.get(i).getFormat().name());
+ final MediaFormat mediaFormat = videoStreams.get(i).getFormat();
+ if (mediaFormat != null) {
+ Log.w(TAG, "No audio stream candidates for video format "
+ + mediaFormat.name());
+ } else {
+ Log.w(TAG, "No audio stream candidates for unknown video format");
+ }
}
}
@@ -288,7 +331,8 @@ public void onServiceDisconnected(final ComponentName name) {
}
@Override
- public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container,
+ public View onCreateView(@NonNull final LayoutInflater inflater,
+ final ViewGroup container,
final Bundle savedInstanceState) {
if (DEBUG) {
Log.d(TAG, "onCreateView() called with: "
@@ -299,14 +343,15 @@ public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup
}
@Override
- public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
+ public void onViewCreated(@NonNull final View view,
+ @Nullable final Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
dialogBinding = DownloadDialogBinding.bind(view);
dialogBinding.fileName.setText(FilenameUtils.createFilename(getContext(),
currentInfo.getName()));
selectedAudioIndex = ListHelper
- .getDefaultAudioFormat(getContext(), currentInfo.getAudioStreams());
+ .getDefaultAudioFormat(getContext(), wrappedAudioStreams.getStreamsList());
selectedSubtitleIndex = getSubtitleIndexBy(subtitleStreamsAdapter.getAll());
@@ -324,7 +369,8 @@ public void onViewCreated(@NonNull final View view, @Nullable final Bundle saved
dialogBinding.threads.setProgress(threads - 1);
dialogBinding.threads.setOnSeekBarChangeListener(new SimpleOnSeekBarChangeListener() {
@Override
- public void onProgressChanged(@NonNull final SeekBar seekbar, final int progress,
+ public void onProgressChanged(@NonNull final SeekBar seekbar,
+ final int progress,
final boolean fromUser) {
final int newProgress = progress + 1;
prefs.edit().putInt(getString(R.string.default_download_threads), newProgress)
@@ -469,7 +515,7 @@ private void requestDownloadPickVideoFolderResult(final ActivityResult result) {
result, getString(R.string.download_path_video_key), DownloadManager.TAG_VIDEO);
}
- private void requestDownloadSaveAsResult(final ActivityResult result) {
+ private void requestDownloadSaveAsResult(@NonNull final ActivityResult result) {
if (result.getResultCode() != Activity.RESULT_OK) {
return;
}
@@ -486,8 +532,8 @@ private void requestDownloadSaveAsResult(final ActivityResult result) {
return;
}
- final DocumentFile docFile
- = DocumentFile.fromSingleUri(context, result.getData().getData());
+ final DocumentFile docFile = DocumentFile.fromSingleUri(context,
+ result.getData().getData());
if (docFile == null) {
showFailedDialog(R.string.general_error);
return;
@@ -498,7 +544,7 @@ private void requestDownloadSaveAsResult(final ActivityResult result) {
docFile.getType());
}
- private void requestDownloadPickFolderResult(final ActivityResult result,
+ private void requestDownloadPickFolderResult(@NonNull final ActivityResult result,
final String key,
final String tag) {
if (result.getResultCode() != Activity.RESULT_OK) {
@@ -518,12 +564,11 @@ private void requestDownloadPickFolderResult(final ActivityResult result,
StoredDirectoryHelper.PERMISSION_FLAGS);
}
- PreferenceManager.getDefaultSharedPreferences(context).edit()
- .putString(key, uri.toString()).apply();
+ PreferenceManager.getDefaultSharedPreferences(context).edit().putString(key,
+ uri.toString()).apply();
try {
- final StoredDirectoryHelper mainStorage
- = new StoredDirectoryHelper(context, uri, tag);
+ final StoredDirectoryHelper mainStorage = new StoredDirectoryHelper(context, uri, tag);
checkSelectedDownload(mainStorage, mainStorage.findFile(filenameTmp),
filenameTmp, mimeTmp);
} catch (final IOException e) {
@@ -561,8 +606,10 @@ public void onCheckedChanged(final RadioGroup group, @IdRes final int checkedId)
}
@Override
- public void onItemSelected(final AdapterView> parent, final View view,
- final int position, final long id) {
+ public void onItemSelected(final AdapterView> parent,
+ final View view,
+ final int position,
+ final long id) {
if (DEBUG) {
Log.d(TAG, "onItemSelected() called with: "
+ "parent = [" + parent + "], view = [" + view + "], "
@@ -597,14 +644,16 @@ protected void setupDownloadOptions() {
final boolean isAudioStreamsAvailable = audioStreamsAdapter.getCount() > 0;
final boolean isSubtitleStreamsAvailable = subtitleStreamsAdapter.getCount() > 0;
- dialogBinding.audioButton.setVisibility(isAudioStreamsAvailable ? View.VISIBLE : View.GONE);
- dialogBinding.videoButton.setVisibility(isVideoStreamsAvailable ? View.VISIBLE : View.GONE);
+ dialogBinding.audioButton.setVisibility(isAudioStreamsAvailable ? View.VISIBLE
+ : View.GONE);
+ dialogBinding.videoButton.setVisibility(isVideoStreamsAvailable ? View.VISIBLE
+ : View.GONE);
dialogBinding.subtitleButton.setVisibility(isSubtitleStreamsAvailable
? View.VISIBLE : View.GONE);
prefs = PreferenceManager.getDefaultSharedPreferences(requireContext());
final String defaultMedia = prefs.getString(getString(R.string.last_used_download_type),
- getString(R.string.last_download_type_video_key));
+ getString(R.string.last_download_type_video_key));
if (isVideoStreamsAvailable
&& (defaultMedia.equals(getString(R.string.last_download_type_video_key)))) {
@@ -640,7 +689,7 @@ private void setRadioButtonsState(final boolean enabled) {
dialogBinding.subtitleButton.setEnabled(enabled);
}
- private int getSubtitleIndexBy(final List streams) {
+ private int getSubtitleIndexBy(@NonNull final List streams) {
final Localization preferredLocalization = NewPipe.getPreferredLocalization();
int candidate = 0;
@@ -666,8 +715,10 @@ private int getSubtitleIndexBy(final List streams) {
return candidate;
}
+ @NonNull
private String getNameEditText() {
- final String str = dialogBinding.fileName.getText().toString().trim();
+ final String str = Objects.requireNonNull(dialogBinding.fileName.getText()).toString()
+ .trim();
return FilenameUtils.createFilename(context, str.isEmpty() ? currentInfo.getName() : str);
}
@@ -683,12 +734,8 @@ private void showFailedDialog(@StringRes final int msg) {
}
private void launchDirectoryPicker(final ActivityResultLauncher launcher) {
- NoFileManagerSafeGuard.launchSafe(
- launcher,
- StoredDirectoryHelper.getPicker(context),
- TAG,
- context
- );
+ NoFileManagerSafeGuard.launchSafe(launcher, StoredDirectoryHelper.getPicker(context), TAG,
+ context);
}
private void prepareSelectedDownload() {
@@ -710,30 +757,46 @@ private void prepareSelectedDownload() {
mimeTmp = "audio/ogg";
filenameTmp += "opus";
} else {
- mimeTmp = format.mimeType;
- filenameTmp += format.suffix;
+ if (format != null) {
+ mimeTmp = format.mimeType;
+ }
+ if (format != null) {
+ filenameTmp += format.suffix;
+ }
}
break;
case R.id.video_button:
selectedMediaType = getString(R.string.last_download_type_video_key);
mainStorage = mainStorageVideo;
format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
- mimeTmp = format.mimeType;
- filenameTmp += format.suffix;
+ if (format != null) {
+ mimeTmp = format.mimeType;
+ }
+ if (format != null) {
+ filenameTmp += format.suffix;
+ }
break;
case R.id.subtitle_button:
selectedMediaType = getString(R.string.last_download_type_subtitle_key);
mainStorage = mainStorageVideo; // subtitle & video files go together
format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat();
- mimeTmp = format.mimeType;
- filenameTmp += (format == MediaFormat.TTML ? MediaFormat.SRT : format).suffix;
+ if (format != null) {
+ mimeTmp = format.mimeType;
+ }
+
+ if (format == MediaFormat.TTML) {
+ filenameTmp += MediaFormat.SRT.suffix;
+ } else {
+ if (format != null) {
+ filenameTmp += format.suffix;
+ }
+ }
break;
default:
throw new RuntimeException("No stream selected");
}
- if (!askForSavePath
- && (mainStorage == null
+ if (!askForSavePath && (mainStorage == null
|| mainStorage.isDirect() == NewPipeSettings.useStorageAccessFramework(context)
|| mainStorage.isInvalidSafStorage())) {
// Pick new download folder if one of:
@@ -767,18 +830,16 @@ private void prepareSelectedDownload() {
initialPath = Uri.parse(initialSavePath.getAbsolutePath());
}
- NoFileManagerSafeGuard.launchSafe(
- requestDownloadSaveAsLauncher,
- StoredFileHelper.getNewPicker(context, filenameTmp, mimeTmp, initialPath),
- TAG,
- context
- );
+ NoFileManagerSafeGuard.launchSafe(requestDownloadSaveAsLauncher,
+ StoredFileHelper.getNewPicker(context, filenameTmp, mimeTmp, initialPath), TAG,
+ context);
return;
}
// check for existing file with the same name
- checkSelectedDownload(mainStorage, mainStorage.findFile(filenameTmp), filenameTmp, mimeTmp);
+ checkSelectedDownload(mainStorage, mainStorage.findFile(filenameTmp), filenameTmp,
+ mimeTmp);
// remember the last media type downloaded by the user
prefs.edit().putString(getString(R.string.last_used_download_type), selectedMediaType)
@@ -786,7 +847,8 @@ private void prepareSelectedDownload() {
}
private void checkSelectedDownload(final StoredDirectoryHelper mainStorage,
- final Uri targetFile, final String filename,
+ final Uri targetFile,
+ final String filename,
final String mime) {
StoredFileHelper storage;
@@ -947,7 +1009,7 @@ private void continueSelectedDownload(@NonNull final StoredFileHelper storage) {
storage.truncate();
}
} catch (final IOException e) {
- Log.e(TAG, "failed to truncate the file: " + storage.getUri().toString(), e);
+ Log.e(TAG, "Failed to truncate the file: " + storage.getUri().toString(), e);
showFailedDialog(R.string.overwrite_failed);
return;
}
@@ -992,8 +1054,8 @@ private void continueSelectedDownload(@NonNull final StoredFileHelper storage) {
}
psArgs = null;
- final long videoSize = wrappedVideoStreams
- .getSizeInBytes((VideoStream) selectedStream);
+ final long videoSize = wrappedVideoStreams.getSizeInBytes(
+ (VideoStream) selectedStream);
// set nearLength, only, if both sizes are fetched or known. This probably
// does not work on slow networks but is later updated in the downloader
@@ -1009,7 +1071,7 @@ private void continueSelectedDownload(@NonNull final StoredFileHelper storage) {
if (selectedStream.getFormat() == MediaFormat.TTML) {
psName = Postprocessing.ALGORITHM_TTML_CONVERTER;
- psArgs = new String[]{
+ psArgs = new String[] {
selectedStream.getFormat().getSuffix(),
"false" // ignore empty frames
};
@@ -1020,17 +1082,22 @@ private void continueSelectedDownload(@NonNull final StoredFileHelper storage) {
}
if (secondaryStream == null) {
- urls = new String[]{
- selectedStream.getUrl()
+ urls = new String[] {
+ selectedStream.getContent()
};
- recoveryInfo = new MissionRecoveryInfo[]{
+ recoveryInfo = new MissionRecoveryInfo[] {
new MissionRecoveryInfo(selectedStream)
};
} else {
- urls = new String[]{
- selectedStream.getUrl(), secondaryStream.getUrl()
+ if (secondaryStream.getDeliveryMethod() != DeliveryMethod.PROGRESSIVE_HTTP) {
+ throw new IllegalArgumentException("Unsupported stream delivery format"
+ + secondaryStream.getDeliveryMethod());
+ }
+
+ urls = new String[] {
+ selectedStream.getContent(), secondaryStream.getContent()
};
- recoveryInfo = new MissionRecoveryInfo[]{new MissionRecoveryInfo(selectedStream),
+ recoveryInfo = new MissionRecoveryInfo[] {new MissionRecoveryInfo(selectedStream),
new MissionRecoveryInfo(secondaryStream)};
}
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java
index 8c260461c2c..f5bd1f363e5 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java
@@ -94,6 +94,7 @@
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.PicassoHelper;
+import org.schabi.newpipe.util.StreamTypeUtil;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.util.external_communication.KoreUtils;
import org.schabi.newpipe.util.external_communication.ShareUtils;
@@ -121,6 +122,7 @@
import static org.schabi.newpipe.player.helper.PlayerHelper.isClearingQueueConfirmationRequired;
import static org.schabi.newpipe.player.playqueue.PlayQueueItem.RECOVERY_UNSET;
import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
+import static org.schabi.newpipe.util.ListHelper.removeNonUrlAndTorrentStreams;
public final class VideoDetailFragment
extends BaseStateFragment
@@ -186,8 +188,7 @@ public final class VideoDetailFragment
@Nullable
private Disposable positionSubscriber = null;
- private List sortedVideoStreams;
- private int selectedVideoStreamIndex = -1;
+ private List videoStreamsForExternalPlayers;
private BottomSheetBehavior bottomSheetBehavior;
private BroadcastReceiver broadcastReceiver;
@@ -1547,11 +1548,13 @@ public void handleResult(@NonNull final StreamInfo info) {
binding.detailSubChannelThumbnailView.setImageDrawable(buddyDrawable);
binding.detailUploaderThumbnailView.setImageDrawable(buddyDrawable);
+ final StreamType streamType = info.getStreamType();
+
if (info.getViewCount() >= 0) {
- if (info.getStreamType().equals(StreamType.AUDIO_LIVE_STREAM)) {
+ if (streamType.equals(StreamType.AUDIO_LIVE_STREAM)) {
binding.detailViewCountView.setText(Localization.listeningCount(activity,
info.getViewCount()));
- } else if (info.getStreamType().equals(StreamType.LIVE_STREAM)) {
+ } else if (streamType.equals(StreamType.LIVE_STREAM)) {
binding.detailViewCountView.setText(Localization
.localizeWatchingCount(activity, info.getViewCount()));
} else {
@@ -1612,14 +1615,13 @@ public void handleResult(@NonNull final StreamInfo info) {
binding.detailToggleSecondaryControlsView.setVisibility(View.VISIBLE);
binding.detailSecondaryControlPanel.setVisibility(View.GONE);
- sortedVideoStreams = ListHelper.getSortedStreamVideosList(
- activity,
- info.getVideoStreams(),
- info.getVideoOnlyStreams(),
- false,
- false);
- selectedVideoStreamIndex = ListHelper
- .getDefaultResolutionIndex(activity, sortedVideoStreams);
+ final List videoStreams = removeNonUrlAndTorrentStreams(
+ new ArrayList<>(currentInfo.getVideoStreams()));
+ final List videoOnlyStreams = removeNonUrlAndTorrentStreams(
+ new ArrayList<>(currentInfo.getVideoOnlyStreams()));
+ videoStreamsForExternalPlayers = ListHelper.getSortedStreamVideosList(activity,
+ videoStreams, videoOnlyStreams, false, false);
+
updateProgressInfo(info);
initThumbnailViews(info);
showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView,
@@ -1645,8 +1647,8 @@ public void handleResult(@NonNull final StreamInfo info) {
}
}
- binding.detailControlsDownload.setVisibility(info.getStreamType() == StreamType.LIVE_STREAM
- || info.getStreamType() == StreamType.AUDIO_LIVE_STREAM ? View.GONE : View.VISIBLE);
+ binding.detailControlsDownload.setVisibility(
+ StreamTypeUtil.isLiveStream(streamType) ? View.GONE : View.VISIBLE);
binding.detailControlsBackground.setVisibility(info.getAudioStreams().isEmpty()
? View.GONE : View.VISIBLE);
@@ -1687,11 +1689,10 @@ public void openDownloadDialog() {
}
try {
- final DownloadDialog downloadDialog = DownloadDialog.newInstance(currentInfo);
- downloadDialog.setVideoStreams(sortedVideoStreams);
- downloadDialog.setAudioStreams(currentInfo.getAudioStreams());
- downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex);
- downloadDialog.setSubtitleStreams(currentInfo.getSubtitles());
+ final DownloadDialog downloadDialog = DownloadDialog.newInstance(activity,
+ currentInfo);
+ downloadDialog.setSelectedVideoStream(ListHelper.getDefaultResolutionIndex(activity,
+ downloadDialog.wrappedVideoStreams.getStreamsList()));
downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
} catch (final Exception e) {
@@ -1722,8 +1723,7 @@ private void updateProgressInfo(@NonNull final StreamInfo info) {
binding.detailPositionView.setVisibility(View.GONE);
// TODO: Remove this check when separation of concerns is done.
// (live streams weren't getting updated because they are mixed)
- if (!info.getStreamType().equals(StreamType.LIVE_STREAM)
- && !info.getStreamType().equals(StreamType.AUDIO_LIVE_STREAM)) {
+ if (!StreamTypeUtil.isLiveStream(info.getStreamType())) {
return;
}
} else {
@@ -2151,25 +2151,33 @@ private void showClearingQueueConfirmation(final Runnable onAllow) {
}
private void showExternalPlaybackDialog() {
- if (sortedVideoStreams == null) {
+ if (currentInfo == null) {
return;
}
- final CharSequence[] resolutions = new CharSequence[sortedVideoStreams.size()];
- for (int i = 0; i < sortedVideoStreams.size(); i++) {
- resolutions[i] = sortedVideoStreams.get(i).getResolution();
- }
- final AlertDialog.Builder builder = new AlertDialog.Builder(activity)
- .setNegativeButton(R.string.cancel, null)
- .setNeutralButton(R.string.open_in_browser, (dialog, i) ->
- ShareUtils.openUrlInBrowser(requireActivity(), url)
- );
- // Maybe there are no video streams available, show just `open in browser` button
- if (resolutions.length > 0) {
- builder.setSingleChoiceItems(resolutions, selectedVideoStreamIndex, (dialog, i) -> {
+
+ final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+ builder.setTitle(R.string.select_quality_external_players);
+ builder.setNegativeButton(android.R.string.cancel, null);
+ builder.setNeutralButton(R.string.open_in_browser, (dialog, i) ->
+ ShareUtils.openUrlInBrowser(requireActivity(), url));
+ if (videoStreamsForExternalPlayers.isEmpty()) {
+ builder.setMessage(R.string.no_video_streams_available_for_external_players);
+ } else {
+ final int selectedVideoStreamIndexForExternalPlayers =
+ ListHelper.getDefaultResolutionIndex(activity, videoStreamsForExternalPlayers);
+ final CharSequence[] resolutions =
+ new CharSequence[videoStreamsForExternalPlayers.size()];
+
+ for (int i = 0; i < videoStreamsForExternalPlayers.size(); i++) {
+ resolutions[i] = videoStreamsForExternalPlayers.get(i).getResolution();
+ }
+
+ builder.setSingleChoiceItems(resolutions, selectedVideoStreamIndexForExternalPlayers,
+ (dialog, i) -> {
dialog.dismiss();
- startOnExternalPlayer(activity, currentInfo, sortedVideoStreams.get(i));
- }
- );
+ startOnExternalPlayer(activity, currentInfo,
+ videoStreamsForExternalPlayers.get(i));
+ });
}
builder.show();
}
diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java
index 79772a6a307..83211d4dd02 100644
--- a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java
+++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java
@@ -96,9 +96,10 @@ public void updateFromItem(final InfoItem infoItem,
case VIDEO_STREAM:
case LIVE_STREAM:
case AUDIO_LIVE_STREAM:
+ case POST_LIVE_STREAM:
+ case POST_LIVE_AUDIO_STREAM:
enableLongClick(item);
break;
- case FILE:
case NONE:
default:
disableLongClick();
@@ -114,7 +115,8 @@ public void updateState(final InfoItem infoItem,
final StreamStateEntity state
= historyRecordManager.loadStreamState(infoItem).blockingGet()[0];
if (state != null && item.getDuration() > 0
- && item.getStreamType() != StreamType.LIVE_STREAM) {
+ && item.getStreamType() != StreamType.LIVE_STREAM
+ && item.getStreamType() != StreamType.AUDIO_LIVE_STREAM) {
itemProgressView.setMax((int) item.getDuration());
if (itemProgressView.getVisibility() == View.VISIBLE) {
itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS
diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/item/StreamItem.kt b/app/src/main/java/org/schabi/newpipe/local/feed/item/StreamItem.kt
index 217e3f3e3c5..96d395aa505 100644
--- a/app/src/main/java/org/schabi/newpipe/local/feed/item/StreamItem.kt
+++ b/app/src/main/java/org/schabi/newpipe/local/feed/item/StreamItem.kt
@@ -14,6 +14,8 @@ import org.schabi.newpipe.databinding.ListStreamItemBinding
import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_LIVE_STREAM
import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_STREAM
import org.schabi.newpipe.extractor.stream.StreamType.LIVE_STREAM
+import org.schabi.newpipe.extractor.stream.StreamType.POST_LIVE_AUDIO_STREAM
+import org.schabi.newpipe.extractor.stream.StreamType.POST_LIVE_STREAM
import org.schabi.newpipe.extractor.stream.StreamType.VIDEO_STREAM
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.PicassoHelper
@@ -109,7 +111,7 @@ data class StreamItem(
}
override fun isLongClickable() = when (stream.streamType) {
- AUDIO_STREAM, VIDEO_STREAM, LIVE_STREAM, AUDIO_LIVE_STREAM -> true
+ AUDIO_STREAM, VIDEO_STREAM, LIVE_STREAM, AUDIO_LIVE_STREAM, POST_LIVE_STREAM, POST_LIVE_AUDIO_STREAM -> true
else -> false
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java
index 583da476464..d2aed76238c 100644
--- a/app/src/main/java/org/schabi/newpipe/player/Player.java
+++ b/app/src/main/java/org/schabi/newpipe/player/Player.java
@@ -1744,24 +1744,9 @@ private void triggerProgressUpdate() {
if (exoPlayerIsNull()) {
return;
}
- // Use duration of currentItem for non-live streams,
- // because HLS streams are fragmented
- // and thus the whole duration is not available to the player
- // TODO: revert #6307 when introducing proper HLS support
- final int duration;
- if (currentItem != null
- && !StreamTypeUtil.isLiveStream(currentItem.getStreamType())
- ) {
- // convert seconds to milliseconds
- duration = (int) (currentItem.getDuration() * 1000);
- } else {
- duration = (int) simpleExoPlayer.getDuration();
- }
- onUpdateProgress(
- Math.max((int) simpleExoPlayer.getCurrentPosition(), 0),
- duration,
- simpleExoPlayer.getBufferedPercentage()
- );
+
+ onUpdateProgress(Math.max((int) simpleExoPlayer.getCurrentPosition(), 0),
+ (int) simpleExoPlayer.getDuration(), simpleExoPlayer.getBufferedPercentage());
}
private Disposable getProgressUpdateDisposable() {
@@ -3399,6 +3384,7 @@ private void updateStreamRelatedViews() {
switch (info.getStreamType()) {
case AUDIO_STREAM:
+ case POST_LIVE_AUDIO_STREAM:
binding.surfaceView.setVisibility(View.GONE);
binding.endScreen.setVisibility(View.VISIBLE);
binding.playbackEndTime.setVisibility(View.VISIBLE);
@@ -3417,6 +3403,7 @@ private void updateStreamRelatedViews() {
break;
case VIDEO_STREAM:
+ case POST_LIVE_STREAM:
if (currentMetadata == null
|| !currentMetadata.getMaybeQuality().isPresent()
|| (info.getVideoStreams().isEmpty()
@@ -3484,10 +3471,10 @@ private void buildQualityMenu() {
for (int i = 0; i < availableStreams.size(); i++) {
final VideoStream videoStream = availableStreams.get(i);
qualityPopupMenu.getMenu().add(POPUP_MENU_ID_QUALITY, i, Menu.NONE, MediaFormat
- .getNameById(videoStream.getFormatId()) + " " + videoStream.resolution);
+ .getNameById(videoStream.getFormatId()) + " " + videoStream.getResolution());
}
if (getSelectedVideoStream() != null) {
- binding.qualityTextView.setText(getSelectedVideoStream().resolution);
+ binding.qualityTextView.setText(getSelectedVideoStream().getResolution());
}
qualityPopupMenu.setOnMenuItemClickListener(this);
qualityPopupMenu.setOnDismissListener(this);
@@ -3605,7 +3592,7 @@ public boolean onMenuItemClick(@NonNull final MenuItem menuItem) {
}
saveStreamProgressState(); //TODO added, check if good
- final String newResolution = availableStreams.get(menuItemIndex).resolution;
+ final String newResolution = availableStreams.get(menuItemIndex).getResolution();
setRecovery();
setPlaybackQuality(newResolution);
reloadPlayQueueManager();
@@ -3633,7 +3620,7 @@ public void onDismiss(@Nullable final PopupMenu menu) {
}
isSomePopupMenuVisible = false; //TODO check if this works
if (getSelectedVideoStream() != null) {
- binding.qualityTextView.setText(getSelectedVideoStream().resolution);
+ binding.qualityTextView.setText(getSelectedVideoStream().getResolution());
}
if (isPlaying()) {
hideControls(DEFAULT_CONTROLS_DURATION, 0);
@@ -4250,7 +4237,8 @@ private void useVideoSource(final boolean videoEnabled) {
} else {
final StreamType streamType = info.getStreamType();
if (streamType == StreamType.AUDIO_STREAM
- || streamType == StreamType.AUDIO_LIVE_STREAM) {
+ || streamType == StreamType.AUDIO_LIVE_STREAM
+ || streamType == StreamType.POST_LIVE_AUDIO_STREAM) {
// Nothing to do more than setting the recovery position
setRecovery();
return;
@@ -4285,13 +4273,15 @@ private void useVideoSource(final boolean videoEnabled) {
* the content is not an audio content, but also if none of the following cases is met:
*
*
- *
the content is an {@link StreamType#AUDIO_STREAM audio stream} or an
- * {@link StreamType#AUDIO_LIVE_STREAM audio live stream};
+ *
the content is an {@link StreamType#AUDIO_STREAM audio stream}, an
+ * {@link StreamType#AUDIO_LIVE_STREAM audio live stream}, or a
+ * {@link StreamType#POST_LIVE_AUDIO_STREAM ended audio live stream};
*
the content is a {@link StreamType#LIVE_STREAM live stream} and the source type is a
* {@link SourceType#LIVE_STREAM live source};
*
the content's source is {@link SourceType#VIDEO_WITH_SEPARATED_AUDIO a video stream
* with a separated audio source} or has no audio-only streams available and is a
- * {@link StreamType#LIVE_STREAM live stream} or a
+ * {@link StreamType#VIDEO_STREAM video stream}, an
+ * {@link StreamType#POST_LIVE_STREAM ended live stream}, or a
* {@link StreamType#LIVE_STREAM live stream}.
*
*
@@ -4309,14 +4299,17 @@ private boolean playQueueManagerReloadingNeeded(final SourceType sourceType,
final StreamType streamType = streamInfo.getStreamType();
if (videoRendererIndex == RENDERER_UNAVAILABLE && streamType != StreamType.AUDIO_STREAM
- && streamType != StreamType.AUDIO_LIVE_STREAM) {
+ && streamType != StreamType.AUDIO_LIVE_STREAM
+ && streamType != StreamType.POST_LIVE_AUDIO_STREAM) {
return true;
}
// The content is an audio stream, an audio live stream, or a live stream with a live
// source: it's not needed to reload the play queue manager because the stream source will
// be the same
- if ((streamType == StreamType.AUDIO_STREAM || streamType == StreamType.AUDIO_LIVE_STREAM)
+ if ((streamType == StreamType.AUDIO_STREAM
+ || streamType == StreamType.POST_LIVE_AUDIO_STREAM
+ || streamType == StreamType.AUDIO_LIVE_STREAM)
|| (streamType == StreamType.LIVE_STREAM
&& sourceType == SourceType.LIVE_STREAM)) {
return false;
@@ -4331,8 +4324,10 @@ private boolean playQueueManagerReloadingNeeded(final SourceType sourceType,
|| (sourceType == SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY
&& isNullOrEmpty(streamInfo.getAudioStreams()))) {
// It's not needed to reload the play queue manager only if the content's stream type
- // is a video stream or a live stream
- return streamType != StreamType.VIDEO_STREAM && streamType != StreamType.LIVE_STREAM;
+ // is a video stream, a live stream or an ended live stream
+ return streamType != StreamType.VIDEO_STREAM
+ && streamType != StreamType.LIVE_STREAM
+ && streamType != StreamType.POST_LIVE_STREAM;
}
// Other cases: the play queue manager reload is needed
diff --git a/app/src/main/java/org/schabi/newpipe/player/datasource/YoutubeHttpDataSource.java b/app/src/main/java/org/schabi/newpipe/player/datasource/YoutubeHttpDataSource.java
new file mode 100644
index 00000000000..acf9c6a4760
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/datasource/YoutubeHttpDataSource.java
@@ -0,0 +1,1031 @@
+/*
+ * Based on ExoPlayer's DefaultHttpDataSource, version 2.17.1.
+ *
+ * Original source code copyright (C) 2016 The Android Open Source Project, licensed under the
+ * Apache License, Version 2.0.
+ */
+
+package org.schabi.newpipe.player.datasource;
+
+import static com.google.android.exoplayer2.upstream.DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS;
+import static com.google.android.exoplayer2.upstream.DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS;
+import static com.google.android.exoplayer2.upstream.HttpUtil.buildRangeRequestHeader;
+import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
+import static com.google.android.exoplayer2.util.Util.castNonNull;
+import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getAndroidUserAgent;
+import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getIosUserAgent;
+import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isAndroidStreamingUrl;
+import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isIosStreamingUrl;
+import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isWebStreamingUrl;
+import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isTvHtml5SimplyEmbeddedPlayerStreamingUrl;
+import static java.lang.Math.min;
+
+import android.net.Uri;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.PlaybackException;
+import com.google.android.exoplayer2.upstream.BaseDataSource;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DataSourceException;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.upstream.DataSpec.HttpMethod;
+import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
+import com.google.android.exoplayer2.upstream.HttpDataSource;
+import com.google.android.exoplayer2.upstream.HttpUtil;
+import com.google.android.exoplayer2.upstream.TransferListener;
+import com.google.android.exoplayer2.util.Log;
+import com.google.android.exoplayer2.util.Util;
+import com.google.common.base.Predicate;
+import com.google.common.collect.ForwardingMap;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Sets;
+import com.google.common.net.HttpHeaders;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InterruptedIOException;
+import java.io.OutputStream;
+import java.lang.reflect.Method;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.NoRouteToHostException;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.zip.GZIPInputStream;
+
+/**
+ * An {@link HttpDataSource} that uses Android's {@link HttpURLConnection}, based on
+ * {@link com.google.android.exoplayer2.upstream.DefaultHttpDataSource}, for YouTube streams.
+ *
+ *
+ * It adds more headers to {@code videoplayback} URLs, such as {@code Origin}, {@code Referer}
+ * (only where it's relevant) and also more parameters, such as {@code rn} and replaces the use of
+ * the {@code Range} header by the corresponding parameter ({@code range}), if enabled.
+ *
+ */
+@SuppressWarnings({"squid:S3011", "squid:S4738"})
+public final class YoutubeHttpDataSource extends BaseDataSource implements HttpDataSource {
+
+ /**
+ * {@link DataSource.Factory} for {@link YoutubeHttpDataSource} instances.
+ */
+ public static final class Factory implements HttpDataSource.Factory {
+
+ private final RequestProperties defaultRequestProperties;
+
+ @Nullable
+ private TransferListener transferListener;
+ @Nullable
+ private Predicate contentTypePredicate;
+ private int connectTimeoutMs;
+ private int readTimeoutMs;
+ private boolean allowCrossProtocolRedirects;
+ private boolean keepPostFor302Redirects;
+
+ @Nullable
+ private String userAgentForNonMobileStreams;
+ private boolean rangeParameterEnabled;
+ private boolean rnParameterEnabled;
+
+ /**
+ * Creates an instance.
+ */
+ public Factory() {
+ defaultRequestProperties = new RequestProperties();
+ connectTimeoutMs = DEFAULT_CONNECT_TIMEOUT_MILLIS;
+ readTimeoutMs = DEFAULT_READ_TIMEOUT_MILLIS;
+ }
+
+ @NonNull
+ @Override
+ public Factory setDefaultRequestProperties(
+ @NonNull final Map defaultRequestPropertiesMap) {
+ defaultRequestProperties.clearAndSet(defaultRequestPropertiesMap);
+ return this;
+ }
+
+ /**
+ * Sets the user agent that will be used, only for non-mobile streams.
+ *
+ *
+ * The default is {@code null}, which causes the default user agent of the underlying
+ * platform to be used.
+ *
+ *
+ * @param userAgentForNonMobileStreamsValue The user agent that will be used for non-mobile
+ * streams, or {@code null} to use the default
+ * user agent of the underlying platform.
+ * @return This factory.
+ */
+ public Factory setUserAgentForNonMobileStreams(
+ @Nullable final String userAgentForNonMobileStreamsValue) {
+ userAgentForNonMobileStreams = userAgentForNonMobileStreamsValue;
+ return this;
+ }
+
+ /**
+ * Sets the connect timeout, in milliseconds.
+ *
+ *
+ * The default is {@link DefaultHttpDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS}.
+ *
+ *
+ * @param connectTimeoutMsValue The connect timeout, in milliseconds, that will be used.
+ * @return This factory.
+ */
+ public Factory setConnectTimeoutMs(final int connectTimeoutMsValue) {
+ connectTimeoutMs = connectTimeoutMsValue;
+ return this;
+ }
+
+ /**
+ * Sets the read timeout, in milliseconds.
+ *
+ *
The default is {@link DefaultHttpDataSource#DEFAULT_READ_TIMEOUT_MILLIS}.
+ *
+ * @param readTimeoutMsValue The connect timeout, in milliseconds, that will be used.
+ * @return This factory.
+ */
+ public Factory setReadTimeoutMs(final int readTimeoutMsValue) {
+ readTimeoutMs = readTimeoutMsValue;
+ return this;
+ }
+
+ /**
+ * Sets whether to allow cross protocol redirects.
+ *
+ *
The default is {@code false}.
+ *
+ * @param allowCrossProtocolRedirectsValue Whether to allow cross protocol redirects.
+ * @return This factory.
+ */
+ public Factory setAllowCrossProtocolRedirects(
+ final boolean allowCrossProtocolRedirectsValue) {
+ allowCrossProtocolRedirects = allowCrossProtocolRedirectsValue;
+ return this;
+ }
+
+ /**
+ * Sets whether the use of the {@code range} parameter instead of the {@code Range} header
+ * to request ranges of streams is enabled.
+ *
+ *
+ * Note that it must be not enabled on streams which are using a {@link
+ * com.google.android.exoplayer2.source.ProgressiveMediaSource}, as it will break playback
+ * for them (some exceptions may be thrown).
+ *
+ *
+ * @param rangeParameterEnabledValue whether the use of the {@code range} parameter instead
+ * of the {@code Range} header (must be only enabled when
+ * non-{@code ProgressiveMediaSource}s)
+ * @return This factory.
+ */
+ public Factory setRangeParameterEnabled(final boolean rangeParameterEnabledValue) {
+ rangeParameterEnabled = rangeParameterEnabledValue;
+ return this;
+ }
+
+ /**
+ * Sets whether the use of the {@code rn}, which stands for request number, parameter is
+ * enabled.
+ *
+ *
+ * Note that it should be not enabled on streams which are using {@code /} to delimit URLs
+ * parameters, such as the streams of HLS manifests.
+ *
+ *
+ * @param rnParameterEnabledValue whether the appending the {@code rn} parameter to
+ * {@code videoplayback} URLs
+ * @return This factory.
+ */
+ public Factory setRnParameterEnabled(final boolean rnParameterEnabledValue) {
+ rnParameterEnabled = rnParameterEnabledValue;
+ return this;
+ }
+
+ /**
+ * Sets a content type {@link Predicate}. If a content type is rejected by the predicate
+ * then a {@link HttpDataSource.InvalidContentTypeException} is thrown from
+ * {@link YoutubeHttpDataSource#open(DataSpec)}.
+ *
+ *
+ * The default is {@code null}.
+ *
+ *
+ * @param contentTypePredicateToSet The content type {@link Predicate}, or {@code null} to
+ * clear a predicate that was previously set.
+ * @return This factory.
+ */
+ public Factory setContentTypePredicate(
+ @Nullable final Predicate contentTypePredicateToSet) {
+ this.contentTypePredicate = contentTypePredicateToSet;
+ return this;
+ }
+
+ /**
+ * Sets the {@link TransferListener} that will be used.
+ *
+ *
The default is {@code null}.
+ *
+ *
See {@link DataSource#addTransferListener(TransferListener)}.
+ *
+ * @param transferListenerToUse The listener that will be used.
+ * @return This factory.
+ */
+ public Factory setTransferListener(
+ @Nullable final TransferListener transferListenerToUse) {
+ this.transferListener = transferListenerToUse;
+ return this;
+ }
+
+ /**
+ * Sets whether we should keep the POST method and body when we have HTTP 302 redirects for
+ * a POST request.
+ *
+ * @param keepPostFor302RedirectsValue Whether we should keep the POST method and body when
+ * we have HTTP 302 redirects for a POST request.
+ * @return This factory.
+ */
+ public Factory setKeepPostFor302Redirects(final boolean keepPostFor302RedirectsValue) {
+ this.keepPostFor302Redirects = keepPostFor302RedirectsValue;
+ return this;
+ }
+
+ @NonNull
+ @Override
+ public YoutubeHttpDataSource createDataSource() {
+ final YoutubeHttpDataSource dataSource = new YoutubeHttpDataSource(
+ userAgentForNonMobileStreams,
+ connectTimeoutMs,
+ readTimeoutMs,
+ allowCrossProtocolRedirects,
+ rangeParameterEnabled,
+ rnParameterEnabled,
+ defaultRequestProperties,
+ contentTypePredicate,
+ keepPostFor302Redirects);
+ if (transferListener != null) {
+ dataSource.addTransferListener(transferListener);
+ }
+ return dataSource;
+ }
+ }
+
+ private static final String TAG = YoutubeHttpDataSource.class.getSimpleName();
+ private static final int MAX_REDIRECTS = 20; // Same limit as okhttp.
+ private static final int HTTP_STATUS_TEMPORARY_REDIRECT = 307;
+ private static final int HTTP_STATUS_PERMANENT_REDIRECT = 308;
+ private static final long MAX_BYTES_TO_DRAIN = 2048;
+
+ private static final String RN_PARAMETER = "&rn=";
+ private static final String YOUTUBE_BASE_URL = "https://www.youtube.com";
+
+ private final boolean allowCrossProtocolRedirects;
+ private final boolean rangeParameterEnabled;
+ private final boolean rnParameterEnabled;
+
+ private final int connectTimeoutMillis;
+ private final int readTimeoutMillis;
+ @Nullable
+ private final String userAgent;
+ @Nullable
+ private final RequestProperties defaultRequestProperties;
+ private final RequestProperties requestProperties;
+ private final boolean keepPostFor302Redirects;
+
+ @Nullable
+ private final Predicate contentTypePredicate;
+ @Nullable
+ private DataSpec dataSpec;
+ @Nullable
+ private HttpURLConnection connection;
+ @Nullable
+ private InputStream inputStream;
+ private boolean opened;
+ private int responseCode;
+ private long bytesToRead;
+ private long bytesRead;
+
+ private long requestNumber;
+
+ @SuppressWarnings("checkstyle:ParameterNumber")
+ private YoutubeHttpDataSource(@Nullable final String userAgent,
+ final int connectTimeoutMillis,
+ final int readTimeoutMillis,
+ final boolean allowCrossProtocolRedirects,
+ final boolean rangeParameterEnabled,
+ final boolean rnParameterEnabled,
+ @Nullable final RequestProperties defaultRequestProperties,
+ @Nullable final Predicate contentTypePredicate,
+ final boolean keepPostFor302Redirects) {
+ super(true);
+ this.userAgent = userAgent;
+ this.connectTimeoutMillis = connectTimeoutMillis;
+ this.readTimeoutMillis = readTimeoutMillis;
+ this.allowCrossProtocolRedirects = allowCrossProtocolRedirects;
+ this.rangeParameterEnabled = rangeParameterEnabled;
+ this.rnParameterEnabled = rnParameterEnabled;
+ this.defaultRequestProperties = defaultRequestProperties;
+ this.contentTypePredicate = contentTypePredicate;
+ this.requestProperties = new RequestProperties();
+ this.keepPostFor302Redirects = keepPostFor302Redirects;
+ this.requestNumber = 0;
+ }
+
+ @Override
+ @Nullable
+ public Uri getUri() {
+ return connection == null ? null : Uri.parse(connection.getURL().toString());
+ }
+
+ @Override
+ public int getResponseCode() {
+ return connection == null || responseCode <= 0 ? -1 : responseCode;
+ }
+
+ @NonNull
+ @Override
+ public Map> getResponseHeaders() {
+ if (connection == null) {
+ return ImmutableMap.of();
+ }
+ // connection.getHeaderFields() always contains a null key with a value like
+ // ["HTTP/1.1 200 OK"]. The response code is available from
+ // HttpURLConnection#getResponseCode() and the HTTP version is fixed when establishing the
+ // connection.
+ // DataSource#getResponseHeaders() doesn't allow null keys in the returned map, so we need
+ // to remove it.
+ // connection.getHeaderFields() returns a special unmodifiable case-insensitive Map
+ // so we can't just remove the null key or make a copy without the null key. Instead we
+ // wrap it in a ForwardingMap subclass that ignores and filters out null keys in the read
+ // methods.
+ return new NullFilteringHeadersMap(connection.getHeaderFields());
+ }
+
+ @Override
+ public void setRequestProperty(@NonNull final String name, @NonNull final String value) {
+ checkNotNull(name);
+ checkNotNull(value);
+ requestProperties.set(name, value);
+ }
+
+ @Override
+ public void clearRequestProperty(@NonNull final String name) {
+ checkNotNull(name);
+ requestProperties.remove(name);
+ }
+
+ @Override
+ public void clearAllRequestProperties() {
+ requestProperties.clear();
+ }
+
+ /**
+ * Opens the source to read the specified data.
+ */
+ @Override
+ public long open(@NonNull final DataSpec dataSpecParameter) throws HttpDataSourceException {
+ this.dataSpec = dataSpecParameter;
+ bytesRead = 0;
+ bytesToRead = 0;
+ transferInitializing(dataSpecParameter);
+
+ final HttpURLConnection httpURLConnection;
+ final String responseMessage;
+ try {
+ this.connection = makeConnection(dataSpec);
+ httpURLConnection = this.connection;
+ responseCode = httpURLConnection.getResponseCode();
+ responseMessage = httpURLConnection.getResponseMessage();
+ } catch (final IOException e) {
+ closeConnectionQuietly();
+ throw HttpDataSourceException.createForIOException(e, dataSpec,
+ HttpDataSourceException.TYPE_OPEN);
+ }
+
+ // Check for a valid response code.
+ if (responseCode < 200 || responseCode > 299) {
+ final Map> headers = httpURLConnection.getHeaderFields();
+ if (responseCode == 416) {
+ final long documentSize = HttpUtil.getDocumentSize(
+ httpURLConnection.getHeaderField(HttpHeaders.CONTENT_RANGE));
+ if (dataSpecParameter.position == documentSize) {
+ opened = true;
+ transferStarted(dataSpecParameter);
+ return dataSpecParameter.length != C.LENGTH_UNSET
+ ? dataSpecParameter.length
+ : 0;
+ }
+ }
+
+ final InputStream errorStream = httpURLConnection.getErrorStream();
+ byte[] errorResponseBody;
+ try {
+ errorResponseBody = errorStream != null
+ ? Util.toByteArray(errorStream)
+ : Util.EMPTY_BYTE_ARRAY;
+ } catch (final IOException e) {
+ errorResponseBody = Util.EMPTY_BYTE_ARRAY;
+ }
+
+ closeConnectionQuietly();
+ final IOException cause = responseCode == 416 ? new DataSourceException(
+ PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE)
+ : null;
+ throw new InvalidResponseCodeException(responseCode, responseMessage, cause, headers,
+ dataSpec, errorResponseBody);
+ }
+
+ // Check for a valid content type.
+ final String contentType = httpURLConnection.getContentType();
+ if (contentTypePredicate != null && !contentTypePredicate.apply(contentType)) {
+ closeConnectionQuietly();
+ throw new InvalidContentTypeException(contentType, dataSpecParameter);
+ }
+
+ final long bytesToSkip;
+ if (!rangeParameterEnabled) {
+ // If we requested a range starting from a non-zero position and received a 200 rather
+ // than a 206, then the server does not support partial requests. We'll need to
+ // manually skip to the requested position.
+ bytesToSkip = responseCode == 200 && dataSpecParameter.position != 0
+ ? dataSpecParameter.position
+ : 0;
+ } else {
+ bytesToSkip = 0;
+ }
+
+
+ // Determine the length of the data to be read, after skipping.
+ final boolean isCompressed = isCompressed(httpURLConnection);
+ if (!isCompressed) {
+ if (dataSpecParameter.length != C.LENGTH_UNSET) {
+ bytesToRead = dataSpecParameter.length;
+ } else {
+ final long contentLength = HttpUtil.getContentLength(
+ httpURLConnection.getHeaderField(HttpHeaders.CONTENT_LENGTH),
+ httpURLConnection.getHeaderField(HttpHeaders.CONTENT_RANGE));
+ bytesToRead = contentLength != C.LENGTH_UNSET
+ ? (contentLength - bytesToSkip)
+ : C.LENGTH_UNSET;
+ }
+ } else {
+ // Gzip is enabled. If the server opts to use gzip then the content length in the
+ // response will be that of the compressed data, which isn't what we want. Always use
+ // the dataSpec length in this case.
+ bytesToRead = dataSpecParameter.length;
+ }
+
+ try {
+ inputStream = httpURLConnection.getInputStream();
+ if (isCompressed) {
+ inputStream = new GZIPInputStream(inputStream);
+ }
+ } catch (final IOException e) {
+ closeConnectionQuietly();
+ throw new HttpDataSourceException(e, dataSpec,
+ PlaybackException.ERROR_CODE_IO_UNSPECIFIED,
+ HttpDataSourceException.TYPE_OPEN);
+ }
+
+ opened = true;
+ transferStarted(dataSpecParameter);
+
+ try {
+ skipFully(bytesToSkip, dataSpec);
+ } catch (final IOException e) {
+ closeConnectionQuietly();
+ if (e instanceof HttpDataSourceException) {
+ throw (HttpDataSourceException) e;
+ }
+ throw new HttpDataSourceException(e, dataSpec,
+ PlaybackException.ERROR_CODE_IO_UNSPECIFIED,
+ HttpDataSourceException.TYPE_OPEN);
+ }
+
+ return bytesToRead;
+ }
+
+ @Override
+ public int read(@NonNull final byte[] buffer, final int offset, final int length)
+ throws HttpDataSourceException {
+ try {
+ return readInternal(buffer, offset, length);
+ } catch (final IOException e) {
+ throw HttpDataSourceException.createForIOException(e, castNonNull(dataSpec),
+ HttpDataSourceException.TYPE_READ);
+ }
+ }
+
+ @Override
+ public void close() throws HttpDataSourceException {
+ try {
+ final InputStream connectionInputStream = this.inputStream;
+ if (connectionInputStream != null) {
+ final long bytesRemaining = bytesToRead == C.LENGTH_UNSET
+ ? C.LENGTH_UNSET
+ : bytesToRead - bytesRead;
+ maybeTerminateInputStream(connection, bytesRemaining);
+
+ try {
+ connectionInputStream.close();
+ } catch (final IOException e) {
+ throw new HttpDataSourceException(e, castNonNull(dataSpec),
+ PlaybackException.ERROR_CODE_IO_UNSPECIFIED,
+ HttpDataSourceException.TYPE_CLOSE);
+ }
+ }
+ } finally {
+ inputStream = null;
+ closeConnectionQuietly();
+ if (opened) {
+ opened = false;
+ transferEnded();
+ }
+ }
+ }
+
+ @NonNull
+ private HttpURLConnection makeConnection(@NonNull final DataSpec dataSpecToUse)
+ throws IOException {
+ URL url = new URL(dataSpecToUse.uri.toString());
+ @HttpMethod int httpMethod = dataSpecToUse.httpMethod;
+ @Nullable byte[] httpBody = dataSpecToUse.httpBody;
+ final long position = dataSpecToUse.position;
+ final long length = dataSpecToUse.length;
+ final boolean allowGzip = dataSpecToUse.isFlagSet(DataSpec.FLAG_ALLOW_GZIP);
+
+ if (!allowCrossProtocolRedirects && !keepPostFor302Redirects) {
+ // HttpURLConnection disallows cross-protocol redirects, but otherwise performs
+ // redirection automatically. This is the behavior we want, so use it.
+ return makeConnection(url, httpMethod, httpBody, position, length, allowGzip, true,
+ dataSpecToUse.httpRequestHeaders);
+ }
+
+ // We need to handle redirects ourselves to allow cross-protocol redirects or to keep the
+ // POST request method for 302.
+ int redirectCount = 0;
+ while (redirectCount++ <= MAX_REDIRECTS) {
+ final HttpURLConnection httpURLConnection = makeConnection(url, httpMethod, httpBody,
+ position, length, allowGzip, false, dataSpecToUse.httpRequestHeaders);
+ final int httpURLConnectionResponseCode = httpURLConnection.getResponseCode();
+ final String location = httpURLConnection.getHeaderField("Location");
+ if ((httpMethod == DataSpec.HTTP_METHOD_GET || httpMethod == DataSpec.HTTP_METHOD_HEAD)
+ && (httpURLConnectionResponseCode == HttpURLConnection.HTTP_MULT_CHOICE
+ || httpURLConnectionResponseCode == HttpURLConnection.HTTP_MOVED_PERM
+ || httpURLConnectionResponseCode == HttpURLConnection.HTTP_MOVED_TEMP
+ || httpURLConnectionResponseCode == HttpURLConnection.HTTP_SEE_OTHER
+ || httpURLConnectionResponseCode == HTTP_STATUS_TEMPORARY_REDIRECT
+ || httpURLConnectionResponseCode == HTTP_STATUS_PERMANENT_REDIRECT)) {
+ httpURLConnection.disconnect();
+ url = handleRedirect(url, location, dataSpecToUse);
+ } else if (httpMethod == DataSpec.HTTP_METHOD_POST
+ && (httpURLConnectionResponseCode == HttpURLConnection.HTTP_MULT_CHOICE
+ || httpURLConnectionResponseCode == HttpURLConnection.HTTP_MOVED_PERM
+ || httpURLConnectionResponseCode == HttpURLConnection.HTTP_MOVED_TEMP
+ || httpURLConnectionResponseCode == HttpURLConnection.HTTP_SEE_OTHER)) {
+ httpURLConnection.disconnect();
+ final boolean shouldKeepPost = keepPostFor302Redirects
+ && responseCode == HttpURLConnection.HTTP_MOVED_TEMP;
+ if (!shouldKeepPost) {
+ // POST request follows the redirect and is transformed into a GET request.
+ httpMethod = DataSpec.HTTP_METHOD_GET;
+ httpBody = null;
+ }
+ url = handleRedirect(url, location, dataSpecToUse);
+ } else {
+ return httpURLConnection;
+ }
+ }
+
+ // If we get here we've been redirected more times than are permitted.
+ throw new HttpDataSourceException(
+ new NoRouteToHostException("Too many redirects: " + redirectCount),
+ dataSpecToUse,
+ PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED,
+ HttpDataSourceException.TYPE_OPEN);
+ }
+
+ /**
+ * Configures a connection and opens it.
+ *
+ * @param url The url to connect to.
+ * @param httpMethod The http method.
+ * @param httpBody The body data, or {@code null} if not required.
+ * @param position The byte offset of the requested data.
+ * @param length The length of the requested data, or {@link C#LENGTH_UNSET}.
+ * @param allowGzip Whether to allow the use of gzip.
+ * @param followRedirects Whether to follow redirects.
+ * @param requestParameters parameters (HTTP headers) to include in request.
+ * @return the connection opened
+ */
+ @SuppressWarnings("checkstyle:ParameterNumber")
+ @NonNull
+ private HttpURLConnection makeConnection(
+ @NonNull final URL url,
+ @HttpMethod final int httpMethod,
+ @Nullable final byte[] httpBody,
+ final long position,
+ final long length,
+ final boolean allowGzip,
+ final boolean followRedirects,
+ final Map requestParameters) throws IOException {
+ String requestUrl = url.toString();
+
+ // Don't add the request number parameter if it has been already added (for instance in
+ // DASH manifests) or if that's not a videoplayback URL
+ final boolean isVideoPlaybackUrl = url.getPath().startsWith("/videoplayback");
+ if (isVideoPlaybackUrl && rnParameterEnabled && !requestUrl.contains(RN_PARAMETER)) {
+ requestUrl += RN_PARAMETER + requestNumber;
+ ++requestNumber;
+ }
+
+ if (rangeParameterEnabled && isVideoPlaybackUrl) {
+ final String rangeParameterBuilt = buildRangeParameter(position, length);
+ if (rangeParameterBuilt != null) {
+ requestUrl += rangeParameterBuilt;
+ }
+ }
+
+ final HttpURLConnection httpURLConnection = openConnection(new URL(requestUrl));
+ httpURLConnection.setConnectTimeout(connectTimeoutMillis);
+ httpURLConnection.setReadTimeout(readTimeoutMillis);
+
+ final Map requestHeaders = new HashMap<>();
+ if (defaultRequestProperties != null) {
+ requestHeaders.putAll(defaultRequestProperties.getSnapshot());
+ }
+ requestHeaders.putAll(requestProperties.getSnapshot());
+ requestHeaders.putAll(requestParameters);
+
+ for (final Map.Entry property : requestHeaders.entrySet()) {
+ httpURLConnection.setRequestProperty(property.getKey(), property.getValue());
+ }
+
+ if (!rangeParameterEnabled) {
+ final String rangeHeader = buildRangeRequestHeader(position, length);
+ if (rangeHeader != null) {
+ httpURLConnection.setRequestProperty(HttpHeaders.RANGE, rangeHeader);
+ }
+ }
+
+ if (isWebStreamingUrl(requestUrl)
+ || isTvHtml5SimplyEmbeddedPlayerStreamingUrl(requestUrl)) {
+ httpURLConnection.setRequestProperty(HttpHeaders.ORIGIN, YOUTUBE_BASE_URL);
+ httpURLConnection.setRequestProperty(HttpHeaders.REFERER, YOUTUBE_BASE_URL);
+ httpURLConnection.setRequestProperty(HttpHeaders.SEC_FETCH_DEST, "empty");
+ httpURLConnection.setRequestProperty(HttpHeaders.SEC_FETCH_MODE, "cors");
+ httpURLConnection.setRequestProperty(HttpHeaders.SEC_FETCH_SITE, "cross-site");
+ }
+
+ httpURLConnection.setRequestProperty(HttpHeaders.TE, "trailers");
+
+ final boolean isAnAndroidStreamingUrl = isAndroidStreamingUrl(requestUrl);
+ final boolean isAnIosStreamingUrl = isIosStreamingUrl(requestUrl);
+ if (isAnAndroidStreamingUrl) {
+ // Improvement which may be done: find the content country used to request YouTube
+ // contents to add it in the user agent instead of using the default
+ httpURLConnection.setRequestProperty(HttpHeaders.USER_AGENT,
+ getAndroidUserAgent(null));
+ } else if (isAnIosStreamingUrl) {
+ httpURLConnection.setRequestProperty(HttpHeaders.USER_AGENT,
+ getIosUserAgent(null));
+ } else if (userAgent != null) {
+ httpURLConnection.setRequestProperty(HttpHeaders.USER_AGENT, userAgent);
+ }
+
+ httpURLConnection.setRequestProperty(HttpHeaders.ACCEPT_ENCODING,
+ allowGzip ? "gzip" : "identity");
+ httpURLConnection.setInstanceFollowRedirects(followRedirects);
+ httpURLConnection.setDoOutput(httpBody != null);
+
+ // Mobile clients uses POST requests to fetch contents
+ httpURLConnection.setRequestMethod(isAnAndroidStreamingUrl || isAnIosStreamingUrl
+ ? "POST"
+ : DataSpec.getStringForHttpMethod(httpMethod));
+
+ if (httpBody != null) {
+ httpURLConnection.setFixedLengthStreamingMode(httpBody.length);
+ httpURLConnection.connect();
+ final OutputStream os = httpURLConnection.getOutputStream();
+ os.write(httpBody);
+ os.close();
+ } else {
+ httpURLConnection.connect();
+ }
+ return httpURLConnection;
+ }
+
+ /**
+ * Creates an {@link HttpURLConnection} that is connected with the {@code url}.
+ *
+ * @param url the {@link URL} to create an {@link HttpURLConnection}
+ * @return an {@link HttpURLConnection} created with the {@code url}
+ */
+ private HttpURLConnection openConnection(@NonNull final URL url) throws IOException {
+ return (HttpURLConnection) url.openConnection();
+ }
+
+ /**
+ * Handles a redirect.
+ *
+ * @param originalUrl The original URL.
+ * @param location The Location header in the response. May be {@code null}.
+ * @param dataSpecToHandleRedirect The {@link DataSpec}.
+ * @return The next URL.
+ * @throws HttpDataSourceException If redirection isn't possible.
+ */
+ @NonNull
+ private URL handleRedirect(final URL originalUrl,
+ @Nullable final String location,
+ final DataSpec dataSpecToHandleRedirect)
+ throws HttpDataSourceException {
+ if (location == null) {
+ throw new HttpDataSourceException("Null location redirect", dataSpecToHandleRedirect,
+ PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED,
+ HttpDataSourceException.TYPE_OPEN);
+ }
+
+ // Form the new url.
+ final URL url;
+ try {
+ url = new URL(originalUrl, location);
+ } catch (final MalformedURLException e) {
+ throw new HttpDataSourceException(e, dataSpecToHandleRedirect,
+ PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED,
+ HttpDataSourceException.TYPE_OPEN);
+ }
+
+ // Check that the protocol of the new url is supported.
+ final String protocol = url.getProtocol();
+ if (!"https".equals(protocol) && !"http".equals(protocol)) {
+ throw new HttpDataSourceException("Unsupported protocol redirect: " + protocol,
+ dataSpecToHandleRedirect,
+ PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED,
+ HttpDataSourceException.TYPE_OPEN);
+ }
+
+ if (!allowCrossProtocolRedirects && !protocol.equals(originalUrl.getProtocol())) {
+ throw new HttpDataSourceException(
+ "Disallowed cross-protocol redirect ("
+ + originalUrl.getProtocol()
+ + " to "
+ + protocol
+ + ")",
+ dataSpecToHandleRedirect,
+ PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED,
+ HttpDataSourceException.TYPE_OPEN);
+ }
+
+ return url;
+ }
+
+ /**
+ * Attempts to skip the specified number of bytes in full.
+ *
+ * @param bytesToSkip The number of bytes to skip.
+ * @param dataSpecToUse The {@link DataSpec}.
+ * @throws IOException If the thread is interrupted during the operation, or if the data ended
+ * before skipping the specified number of bytes.
+ */
+ @SuppressWarnings("checkstyle:FinalParameters")
+ private void skipFully(long bytesToSkip, final DataSpec dataSpecToUse) throws IOException {
+ if (bytesToSkip == 0) {
+ return;
+ }
+
+ final byte[] skipBuffer = new byte[4096];
+ while (bytesToSkip > 0) {
+ final int readLength = (int) min(bytesToSkip, skipBuffer.length);
+ final int read = castNonNull(inputStream).read(skipBuffer, 0, readLength);
+ if (Thread.currentThread().isInterrupted()) {
+ throw new HttpDataSourceException(
+ new InterruptedIOException(),
+ dataSpecToUse,
+ PlaybackException.ERROR_CODE_IO_UNSPECIFIED,
+ HttpDataSourceException.TYPE_OPEN);
+ }
+
+ if (read == -1) {
+ throw new HttpDataSourceException(
+ dataSpecToUse,
+ PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE,
+ HttpDataSourceException.TYPE_OPEN);
+ }
+
+ bytesToSkip -= read;
+ bytesTransferred(read);
+ }
+ }
+
+ /**
+ * Reads up to {@code length} bytes of data and stores them into {@code buffer}, starting at
+ * index {@code offset}.
+ *
+ *
+ * This method blocks until at least one byte of data can be read, the end of the opened range
+ * is detected, or an exception is thrown.
+ *
+ *
+ * @param buffer The buffer into which the read data should be stored.
+ * @param offset The start offset into {@code buffer} at which data should be written.
+ * @param readLength The maximum number of bytes to read.
+ * @return The number of bytes read, or {@link C#RESULT_END_OF_INPUT} if the end of the opened
+ * range is reached.
+ * @throws IOException If an error occurs reading from the source.
+ */
+ @SuppressWarnings("checkstyle:FinalParameters")
+ private int readInternal(final byte[] buffer, final int offset, int readLength)
+ throws IOException {
+ if (readLength == 0) {
+ return 0;
+ }
+ if (bytesToRead != C.LENGTH_UNSET) {
+ final long bytesRemaining = bytesToRead - bytesRead;
+ if (bytesRemaining == 0) {
+ return C.RESULT_END_OF_INPUT;
+ }
+ readLength = (int) min(readLength, bytesRemaining);
+ }
+
+ final int read = castNonNull(inputStream).read(buffer, offset, readLength);
+ if (read == -1) {
+ return C.RESULT_END_OF_INPUT;
+ }
+
+ bytesRead += read;
+ bytesTransferred(read);
+ return read;
+ }
+
+ /**
+ * On platform API levels 19 and 20, okhttp's implementation of {@link InputStream#close} can
+ * block for a long time if the stream has a lot of data remaining. Call this method before
+ * closing the input stream to make a best effort to cause the input stream to encounter an
+ * unexpected end of input, working around this issue. On other platform API levels, the method
+ * does nothing.
+ *
+ * @param connection The connection whose {@link InputStream} should be terminated.
+ * @param bytesRemaining The number of bytes remaining to be read from the input stream if its
+ * length is known. {@link C#LENGTH_UNSET} otherwise.
+ */
+ private static void maybeTerminateInputStream(@Nullable final HttpURLConnection connection,
+ final long bytesRemaining) {
+ if (connection == null || Util.SDK_INT < 19 || Util.SDK_INT > 20) {
+ return;
+ }
+
+ try {
+ final InputStream inputStream = connection.getInputStream();
+ if (bytesRemaining == C.LENGTH_UNSET) {
+ // If the input stream has already ended, do nothing. The socket may be re-used.
+ if (inputStream.read() == -1) {
+ return;
+ }
+ } else if (bytesRemaining <= MAX_BYTES_TO_DRAIN) {
+ // There isn't much data left. Prefer to allow it to drain, which may allow the
+ // socket to be re-used.
+ return;
+ }
+ final String className = inputStream.getClass().getName();
+ if ("com.android.okhttp.internal.http.HttpTransport$ChunkedInputStream"
+ .equals(className)
+ || "com.android.okhttp.internal.http.HttpTransport$FixedLengthInputStream"
+ .equals(className)) {
+ final Class> superclass = inputStream.getClass().getSuperclass();
+ final Method unexpectedEndOfInput = checkNotNull(superclass).getDeclaredMethod(
+ "unexpectedEndOfInput");
+ unexpectedEndOfInput.setAccessible(true);
+ unexpectedEndOfInput.invoke(inputStream);
+ }
+ } catch (final Exception e) {
+ // If an IOException then the connection didn't ever have an input stream, or it was
+ // closed already. If another type of exception then something went wrong, most likely
+ // the device isn't using okhttp.
+ }
+ }
+
+ /**
+ * Closes the current connection quietly, if there is one.
+ */
+ private void closeConnectionQuietly() {
+ if (connection != null) {
+ try {
+ connection.disconnect();
+ } catch (final Exception e) {
+ Log.e(TAG, "Unexpected error while disconnecting", e);
+ }
+ connection = null;
+ }
+ }
+
+ private static boolean isCompressed(@NonNull final HttpURLConnection connection) {
+ final String contentEncoding = connection.getHeaderField("Content-Encoding");
+ return "gzip".equalsIgnoreCase(contentEncoding);
+ }
+
+ /**
+ * Builds a {@code range} parameter for the given position and length.
+ *
+ *
+ * To fetch its contents, YouTube use range requests which append a {@code range} parameter
+ * to videoplayback URLs instead of the {@code Range} header (even if the server respond
+ * correctly when requesting a range of a ressouce with it).
+ *
+ *
+ *
+ * The parameter works in the same way as the header.
+ *
+ *
+ * @param position The request position.
+ * @param length The request length, or {@link C#LENGTH_UNSET} if the request is unbounded.
+ * @return The corresponding {@code range} parameter, or {@code null} if this parameter is
+ * unnecessary because the whole resource is being requested.
+ */
+ @Nullable
+ private static String buildRangeParameter(final long position, final long length) {
+ if (position == 0 && length == C.LENGTH_UNSET) {
+ return null;
+ }
+
+ final StringBuilder rangeParameter = new StringBuilder();
+ rangeParameter.append("&range=");
+ rangeParameter.append(position);
+ rangeParameter.append("-");
+ if (length != C.LENGTH_UNSET) {
+ rangeParameter.append(position + length - 1);
+ }
+ return rangeParameter.toString();
+ }
+
+ private static final class NullFilteringHeadersMap
+ extends ForwardingMap> {
+ private final Map> headers;
+
+ NullFilteringHeadersMap(final Map> headers) {
+ this.headers = headers;
+ }
+
+ @NonNull
+ @Override
+ protected Map> delegate() {
+ return headers;
+ }
+
+ @Override
+ public boolean containsKey(@Nullable final Object key) {
+ return key != null && super.containsKey(key);
+ }
+
+ @Nullable
+ @Override
+ public List get(@Nullable final Object key) {
+ return key == null ? null : super.get(key);
+ }
+
+ @NonNull
+ @Override
+ public Set keySet() {
+ return Sets.filter(super.keySet(), Objects::nonNull);
+ }
+
+ @NonNull
+ @Override
+ public Set>> entrySet() {
+ return Sets.filter(super.entrySet(), entry -> entry.getKey() != null);
+ }
+
+ @Override
+ public int size() {
+ return super.size() - (super.containsKey(null) ? 1 : 0);
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return super.isEmpty() || (super.size() == 1 && super.containsKey(null));
+ }
+
+ @Override
+ public boolean containsValue(@Nullable final Object value) {
+ return super.standardContainsValue(value);
+ }
+
+ @Override
+ public boolean equals(@Nullable final Object object) {
+ return object != null && super.standardEquals(object);
+ }
+
+ @Override
+ public int hashCode() {
+ return super.standardHashCode();
+ }
+ }
+}
+
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java b/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java
index 98e04d4661d..47371533ab7 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java
@@ -3,6 +3,9 @@
import android.content.Context;
import android.util.Log;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
import com.google.android.exoplayer2.database.StandaloneDatabaseProvider;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSource;
@@ -14,45 +17,58 @@
import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor;
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
-import java.io.File;
+import org.schabi.newpipe.player.datasource.YoutubeHttpDataSource;
-import androidx.annotation.NonNull;
+import java.io.File;
-/* package-private */ class CacheFactory implements DataSource.Factory {
- private static final String TAG = "CacheFactory";
+/* package-private */ final class CacheFactory implements DataSource.Factory {
+ private static final String TAG = CacheFactory.class.getSimpleName();
private static final String CACHE_FOLDER_NAME = "exoplayer";
- private static final int CACHE_FLAGS = CacheDataSource.FLAG_BLOCK_ON_CACHE
- | CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR;
+ private static final int CACHE_FLAGS = CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR;
+ private static SimpleCache cache;
- private final DataSource.Factory dataSourceFactory;
- private final File cacheDir;
private final long maxFileSize;
+ private final Context context;
+ private final String userAgent;
+ private final TransferListener transferListener;
+ private final DataSource.Factory upstreamDataSourceFactory;
+
+ public static class Builder {
+ private final Context context;
+ private final String userAgent;
+ private final TransferListener transferListener;
+ private DataSource.Factory upstreamDataSourceFactory;
+
+ Builder(@NonNull final Context context,
+ @NonNull final String userAgent,
+ @NonNull final TransferListener transferListener) {
+ this.context = context;
+ this.userAgent = userAgent;
+ this.transferListener = transferListener;
+ }
- // Creating cache on every instance may cause problems with multiple players when
- // sources are not ExtractorMediaSource
- // see: https://stackoverflow.com/questions/28700391/using-cache-in-exoplayer
- // todo: make this a singleton?
- private static SimpleCache cache;
+ public void setUpstreamDataSourceFactory(
+ @Nullable final DataSource.Factory upstreamDataSourceFactory) {
+ this.upstreamDataSourceFactory = upstreamDataSourceFactory;
+ }
- CacheFactory(@NonNull final Context context,
- @NonNull final String userAgent,
- @NonNull final TransferListener transferListener) {
- this(context, userAgent, transferListener, PlayerHelper.getPreferredCacheSize(),
- PlayerHelper.getPreferredFileSize());
+ public CacheFactory build() {
+ return new CacheFactory(context, userAgent, transferListener,
+ upstreamDataSourceFactory);
+ }
}
private CacheFactory(@NonNull final Context context,
@NonNull final String userAgent,
@NonNull final TransferListener transferListener,
- final long maxCacheSize,
- final long maxFileSize) {
- this.maxFileSize = maxFileSize;
-
- dataSourceFactory = new DefaultDataSource
- .Factory(context, new DefaultHttpDataSource.Factory().setUserAgent(userAgent))
- .setTransferListener(transferListener);
- cacheDir = new File(context.getExternalCacheDir(), CACHE_FOLDER_NAME);
+ @Nullable final DataSource.Factory upstreamDataSourceFactory) {
+ this.context = context;
+ this.userAgent = userAgent;
+ this.transferListener = transferListener;
+ this.upstreamDataSourceFactory = upstreamDataSourceFactory;
+
+ final File cacheDir = new File(context.getExternalCacheDir(), CACHE_FOLDER_NAME);
if (!cacheDir.exists()) {
//noinspection ResultOfMethodCallIgnored
cacheDir.mkdir();
@@ -60,37 +76,43 @@ private CacheFactory(@NonNull final Context context,
if (cache == null) {
final LeastRecentlyUsedCacheEvictor evictor
- = new LeastRecentlyUsedCacheEvictor(maxCacheSize);
+ = new LeastRecentlyUsedCacheEvictor(PlayerHelper.getPreferredCacheSize());
cache = new SimpleCache(cacheDir, evictor, new StandaloneDatabaseProvider(context));
+ Log.d(TAG, "initExoPlayerCache: cacheDir = " + cacheDir.getAbsolutePath());
}
+
+ maxFileSize = PlayerHelper.getPreferredFileSize();
}
@NonNull
@Override
public DataSource createDataSource() {
- Log.d(TAG, "initExoPlayerCache: cacheDir = " + cacheDir.getAbsolutePath());
- final DataSource dataSource = dataSourceFactory.createDataSource();
- final FileDataSource fileSource = new FileDataSource();
- final CacheDataSink dataSink = new CacheDataSink(cache, maxFileSize);
-
- return new CacheDataSource(cache, dataSource, fileSource, dataSink, CACHE_FLAGS, null);
- }
-
- public void tryDeleteCacheFiles() {
- if (!cacheDir.exists() || !cacheDir.isDirectory()) {
- return;
+ final DataSource.Factory upstreamDataSourceFactoryToUse;
+ if (upstreamDataSourceFactory == null) {
+ upstreamDataSourceFactoryToUse = new DefaultHttpDataSource.Factory()
+ .setUserAgent(userAgent);
+ } else {
+ if (upstreamDataSourceFactory instanceof DefaultHttpDataSource.Factory) {
+ upstreamDataSourceFactoryToUse =
+ ((DefaultHttpDataSource.Factory) upstreamDataSourceFactory)
+ .setUserAgent(userAgent);
+ } else if (upstreamDataSourceFactory instanceof YoutubeHttpDataSource.Factory) {
+ upstreamDataSourceFactoryToUse =
+ ((YoutubeHttpDataSource.Factory) upstreamDataSourceFactory)
+ .setUserAgentForNonMobileStreams(userAgent);
+ } else {
+ upstreamDataSourceFactoryToUse = upstreamDataSourceFactory;
+ }
}
- try {
- for (final File file : cacheDir.listFiles()) {
- final String filePath = file.getAbsolutePath();
- final boolean deleteSuccessful = file.delete();
+ final DefaultDataSource dataSource = new DefaultDataSource.Factory(context,
+ upstreamDataSourceFactoryToUse)
+ .setTransferListener(transferListener)
+ .createDataSource();
- Log.d(TAG, "tryDeleteCacheFiles: " + filePath + " deleted = " + deleteSuccessful);
- }
- } catch (final Exception e) {
- Log.e(TAG, "Failed to delete file.", e);
- }
+ final FileDataSource fileSource = new FileDataSource();
+ final CacheDataSink dataSink = new CacheDataSink(cache, maxFileSize);
+ return new CacheDataSource(cache, dataSource, fileSource, dataSink, CACHE_FLAGS, null);
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/NonUriHlsPlaylistParserFactory.java b/app/src/main/java/org/schabi/newpipe/player/helper/NonUriHlsPlaylistParserFactory.java
new file mode 100644
index 00000000000..a3a25fd1df8
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/NonUriHlsPlaylistParserFactory.java
@@ -0,0 +1,50 @@
+package org.schabi.newpipe.player.helper;
+
+import android.net.Uri;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist;
+import com.google.android.exoplayer2.source.hls.playlist.HlsMultivariantPlaylist;
+import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist;
+import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParserFactory;
+import com.google.android.exoplayer2.upstream.ParsingLoadable;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A {@link HlsPlaylistParserFactory} for non-URI HLS sources.
+ */
+public final class NonUriHlsPlaylistParserFactory implements HlsPlaylistParserFactory {
+
+ private final HlsPlaylist hlsPlaylist;
+
+ public NonUriHlsPlaylistParserFactory(final HlsPlaylist hlsPlaylist) {
+ this.hlsPlaylist = hlsPlaylist;
+ }
+
+ private final class NonUriHlsPlayListParser implements ParsingLoadable.Parser {
+
+ @Override
+ public HlsPlaylist parse(final Uri uri,
+ final InputStream inputStream) throws IOException {
+ return hlsPlaylist;
+ }
+ }
+
+ @NonNull
+ @Override
+ public ParsingLoadable.Parser createPlaylistParser() {
+ return new NonUriHlsPlayListParser();
+ }
+
+ @NonNull
+ @Override
+ public ParsingLoadable.Parser createPlaylistParser(
+ @NonNull final HlsMultivariantPlaylist multivariantPlaylist,
+ @Nullable final HlsMediaPlaylist previousMediaPlaylist) {
+ return new NonUriHlsPlayListParser();
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java
index 405f6fd37b7..61d8baffcd8 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java
@@ -2,21 +2,27 @@
import android.content.Context;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.source.SingleSampleMediaSource;
import com.google.android.exoplayer2.source.dash.DashMediaSource;
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
import com.google.android.exoplayer2.source.hls.playlist.DefaultHlsPlaylistTracker;
+import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParserFactory;
import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource;
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSource;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
-import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
import com.google.android.exoplayer2.upstream.TransferListener;
-import androidx.annotation.NonNull;
+import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeOtfDashManifestCreator;
+import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubePostLiveStreamDvrDashManifestCreator;
+import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeProgressiveDashManifestCreator;
+import org.schabi.newpipe.player.datasource.YoutubeHttpDataSource;
public class PlayerDataSource {
@@ -29,79 +35,120 @@ public class PlayerDataSource {
* early.
*/
private static final double PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT = 15;
- private static final int MANIFEST_MINIMUM_RETRY = 5;
- private static final int EXTRACTOR_MINIMUM_RETRY = Integer.MAX_VALUE;
+
+ /**
+ * The maximum number of generated manifests per cache, in
+ * {@link YoutubeProgressiveDashManifestCreator}, {@link YoutubeOtfDashManifestCreator} and
+ * {@link YoutubePostLiveStreamDvrDashManifestCreator}.
+ */
+ private static final int MAXIMUM_SIZE_CACHED_GENERATED_MANIFESTS_PER_CACHE = 500;
private final int continueLoadingCheckIntervalBytes;
- private final DataSource.Factory cacheDataSourceFactory;
+ private final CacheFactory.Builder cacheDataSourceFactoryBuilder;
private final DataSource.Factory cachelessDataSourceFactory;
public PlayerDataSource(@NonNull final Context context,
@NonNull final String userAgent,
@NonNull final TransferListener transferListener) {
continueLoadingCheckIntervalBytes = PlayerHelper.getProgressiveLoadIntervalBytes(context);
- cacheDataSourceFactory = new CacheFactory(context, userAgent, transferListener);
- cachelessDataSourceFactory = new DefaultDataSource
- .Factory(context, new DefaultHttpDataSource.Factory().setUserAgent(userAgent))
+ cacheDataSourceFactoryBuilder = new CacheFactory.Builder(context, userAgent,
+ transferListener);
+ cachelessDataSourceFactory = new DefaultDataSource.Factory(context,
+ new DefaultHttpDataSource.Factory().setUserAgent(userAgent))
.setTransferListener(transferListener);
+
+ YoutubeProgressiveDashManifestCreator.getCache().setMaximumSize(
+ MAXIMUM_SIZE_CACHED_GENERATED_MANIFESTS_PER_CACHE);
+ YoutubeOtfDashManifestCreator.getCache().setMaximumSize(
+ MAXIMUM_SIZE_CACHED_GENERATED_MANIFESTS_PER_CACHE);
+ YoutubePostLiveStreamDvrDashManifestCreator.getCache().setMaximumSize(
+ MAXIMUM_SIZE_CACHED_GENERATED_MANIFESTS_PER_CACHE);
}
public SsMediaSource.Factory getLiveSsMediaSourceFactory() {
- return new SsMediaSource.Factory(
- new DefaultSsChunkSource.Factory(cachelessDataSourceFactory),
- cachelessDataSourceFactory
- )
- .setLoadErrorHandlingPolicy(
- new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY))
- .setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS);
+ return getSSMediaSourceFactory().setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS);
}
public HlsMediaSource.Factory getLiveHlsMediaSourceFactory() {
return new HlsMediaSource.Factory(cachelessDataSourceFactory)
.setAllowChunklessPreparation(true)
- .setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(
- MANIFEST_MINIMUM_RETRY))
.setPlaylistTrackerFactory((dataSourceFactory, loadErrorHandlingPolicy,
playlistParserFactory) ->
new DefaultHlsPlaylistTracker(dataSourceFactory, loadErrorHandlingPolicy,
- playlistParserFactory, PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT)
- );
+ playlistParserFactory,
+ PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT));
}
public DashMediaSource.Factory getLiveDashMediaSourceFactory() {
return new DashMediaSource.Factory(
getDefaultDashChunkSourceFactory(cachelessDataSourceFactory),
- cachelessDataSourceFactory
- )
- .setLoadErrorHandlingPolicy(
- new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY));
+ cachelessDataSourceFactory);
}
- private DefaultDashChunkSource.Factory getDefaultDashChunkSourceFactory(
- final DataSource.Factory dataSourceFactory
- ) {
- return new DefaultDashChunkSource.Factory(dataSourceFactory);
+ public HlsMediaSource.Factory getHlsMediaSourceFactory(
+ @Nullable final HlsPlaylistParserFactory hlsPlaylistParserFactory) {
+ final HlsMediaSource.Factory factory = new HlsMediaSource.Factory(
+ cacheDataSourceFactoryBuilder.build());
+ if (hlsPlaylistParserFactory != null) {
+ factory.setPlaylistParserFactory(hlsPlaylistParserFactory);
+ }
+ return factory;
}
- public HlsMediaSource.Factory getHlsMediaSourceFactory() {
- return new HlsMediaSource.Factory(cacheDataSourceFactory);
+ public DashMediaSource.Factory getDashMediaSourceFactory() {
+ return new DashMediaSource.Factory(
+ getDefaultDashChunkSourceFactory(cacheDataSourceFactoryBuilder.build()),
+ cacheDataSourceFactoryBuilder.build());
}
- public DashMediaSource.Factory getDashMediaSourceFactory() {
+ public ProgressiveMediaSource.Factory getProgressiveMediaSourceFactory() {
+ return new ProgressiveMediaSource.Factory(cacheDataSourceFactoryBuilder.build())
+ .setContinueLoadingCheckIntervalBytes(continueLoadingCheckIntervalBytes);
+ }
+
+ public SsMediaSource.Factory getSSMediaSourceFactory() {
+ return new SsMediaSource.Factory(
+ new DefaultSsChunkSource.Factory(cachelessDataSourceFactory),
+ cachelessDataSourceFactory);
+ }
+
+ public SingleSampleMediaSource.Factory getSingleSampleMediaSourceFactory() {
+ return new SingleSampleMediaSource.Factory(cacheDataSourceFactoryBuilder.build());
+ }
+
+ public DashMediaSource.Factory getYoutubeDashMediaSourceFactory() {
+ cacheDataSourceFactoryBuilder.setUpstreamDataSourceFactory(
+ getYoutubeHttpDataSourceFactory(true, true));
return new DashMediaSource.Factory(
- getDefaultDashChunkSourceFactory(cacheDataSourceFactory),
- cacheDataSourceFactory
- );
+ getDefaultDashChunkSourceFactory(cacheDataSourceFactoryBuilder.build()),
+ cacheDataSourceFactoryBuilder.build());
+ }
+
+ public HlsMediaSource.Factory getYoutubeHlsMediaSourceFactory() {
+ cacheDataSourceFactoryBuilder.setUpstreamDataSourceFactory(
+ getYoutubeHttpDataSourceFactory(false, false));
+ return new HlsMediaSource.Factory(cacheDataSourceFactoryBuilder.build());
}
- public ProgressiveMediaSource.Factory getExtractorMediaSourceFactory() {
- return new ProgressiveMediaSource.Factory(cacheDataSourceFactory)
- .setContinueLoadingCheckIntervalBytes(continueLoadingCheckIntervalBytes)
- .setLoadErrorHandlingPolicy(
- new DefaultLoadErrorHandlingPolicy(EXTRACTOR_MINIMUM_RETRY));
+ public ProgressiveMediaSource.Factory getYoutubeProgressiveMediaSourceFactory() {
+ cacheDataSourceFactoryBuilder.setUpstreamDataSourceFactory(
+ getYoutubeHttpDataSourceFactory(false, true));
+ return new ProgressiveMediaSource.Factory(cacheDataSourceFactoryBuilder.build())
+ .setContinueLoadingCheckIntervalBytes(continueLoadingCheckIntervalBytes);
+ }
+
+ @NonNull
+ private DefaultDashChunkSource.Factory getDefaultDashChunkSourceFactory(
+ final DataSource.Factory dataSourceFactory) {
+ return new DefaultDashChunkSource.Factory(dataSourceFactory);
}
- public SingleSampleMediaSource.Factory getSampleMediaSourceFactory() {
- return new SingleSampleMediaSource.Factory(cacheDataSourceFactory);
+ @NonNull
+ private YoutubeHttpDataSource.Factory getYoutubeHttpDataSourceFactory(
+ final boolean rangeParameterEnabled,
+ final boolean rnParameterEnabled) {
+ return new YoutubeHttpDataSource.Factory()
+ .setRangeParameterEnabled(rangeParameterEnabled)
+ .setRnParameterEnabled(rnParameterEnabled);
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java
index b73c6cf7f0b..d924f931476 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java
@@ -3,6 +3,8 @@
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF;
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
+import static org.schabi.newpipe.extractor.stream.AudioStream.UNKNOWN_BITRATE;
+import static org.schabi.newpipe.extractor.stream.VideoStream.RESOLUTION_UNKNOWN;
import static org.schabi.newpipe.player.Player.IDLE_WINDOW_FLAGS;
import static org.schabi.newpipe.player.Player.PLAYER_TYPE;
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_ALWAYS;
@@ -110,12 +112,14 @@ public final class PlayerHelper {
int MINIMIZE_ON_EXIT_MODE_POPUP = 2;
}
- private PlayerHelper() { }
+ private PlayerHelper() {
+ }
////////////////////////////////////////////////////////////////////////////
// Exposed helpers
////////////////////////////////////////////////////////////////////////////
+ @NonNull
public static String getTimeString(final int milliSeconds) {
final int seconds = (milliSeconds % 60000) / 1000;
final int minutes = (milliSeconds % 3600000) / 60000;
@@ -131,15 +135,18 @@ public static String getTimeString(final int milliSeconds) {
).toString();
}
+ @NonNull
public static String formatSpeed(final double speed) {
return SPEED_FORMATTER.format(speed);
}
+ @NonNull
public static String formatPitch(final double pitch) {
return PITCH_FORMATTER.format(pitch);
}
- public static String subtitleMimeTypesOf(final MediaFormat format) {
+ @NonNull
+ public static String subtitleMimeTypesOf(@NonNull final MediaFormat format) {
switch (format) {
case VTT:
return MimeTypes.TEXT_VTT;
@@ -192,14 +199,48 @@ public static String resizeTypeOf(@NonNull final Context context,
@NonNull
public static String cacheKeyOf(@NonNull final StreamInfo info,
- @NonNull final VideoStream video) {
- return info.getUrl() + video.getResolution() + video.getFormat().getName();
+ @NonNull final VideoStream videoStream) {
+ String cacheKey = info.getUrl() + " " + videoStream.getId();
+
+ final String resolution = videoStream.getResolution();
+ final MediaFormat mediaFormat = videoStream.getFormat();
+ if (resolution.equals(RESOLUTION_UNKNOWN) && mediaFormat == null) {
+ // The hash code is only used in the cache key in the case when the resolution and the
+ // media format are unknown
+ cacheKey += " " + videoStream.hashCode();
+ } else {
+ if (mediaFormat != null) {
+ cacheKey += " " + videoStream.getFormat().getName();
+ }
+ if (!resolution.equals(RESOLUTION_UNKNOWN)) {
+ cacheKey += " " + resolution;
+ }
+ }
+
+ return cacheKey;
}
@NonNull
public static String cacheKeyOf(@NonNull final StreamInfo info,
- @NonNull final AudioStream audio) {
- return info.getUrl() + audio.getAverageBitrate() + audio.getFormat().getName();
+ @NonNull final AudioStream audioStream) {
+ String cacheKey = info.getUrl() + " " + audioStream.getId();
+
+ final int averageBitrate = audioStream.getAverageBitrate();
+ final MediaFormat mediaFormat = audioStream.getFormat();
+ if (averageBitrate == UNKNOWN_BITRATE && mediaFormat == null) {
+ // The hash code is only used in the cache key in the case when the resolution and the
+ // media format are unknown
+ cacheKey += " " + audioStream.hashCode();
+ } else {
+ if (mediaFormat != null) {
+ cacheKey += " " + audioStream.getFormat().getName();
+ }
+ if (averageBitrate != UNKNOWN_BITRATE) {
+ cacheKey += " " + averageBitrate;
+ }
+ }
+
+ return cacheKey;
}
/**
@@ -233,7 +274,7 @@ public static PlayQueue autoQueueOf(@NonNull final StreamInfo info,
return null;
}
- if (relatedItems.get(0) != null && relatedItems.get(0) instanceof StreamInfoItem
+ if (relatedItems.get(0) instanceof StreamInfoItem
&& !urls.contains(relatedItems.get(0).getUrl())) {
return getAutoQueuedSinglePlayQueue((StreamInfoItem) relatedItems.get(0));
}
@@ -335,6 +376,7 @@ public static long getPreferredFileSize() {
return 2 * 1024 * 1024L; // ExoPlayer CacheDataSink.MIN_RECOMMENDED_FRAGMENT_SIZE
}
+ @NonNull
public static ExoTrackSelection.Factory getQualitySelector() {
return new AdaptiveTrackSelection.Factory(
1000,
@@ -389,7 +431,7 @@ public static float getCaptionScale(@NonNull final Context context) {
/**
* @param context the Android context
* @return the screen brightness to use. A value less than 0 (the default) means to use the
- * preferred screen brightness
+ * preferred screen brightness
*/
public static float getScreenBrightness(@NonNull final Context context) {
final SharedPreferences sp = getPreferences(context);
@@ -480,7 +522,8 @@ public static int nextRepeatMode(@RepeatMode final int repeatMode) {
return REPEAT_MODE_ONE;
case REPEAT_MODE_ONE:
return REPEAT_MODE_ALL;
- case REPEAT_MODE_ALL: default:
+ case REPEAT_MODE_ALL:
+ default:
return REPEAT_MODE_OFF;
}
}
@@ -548,7 +591,7 @@ public static WindowManager.LayoutParams retrievePopupLayoutParamsFromPrefs(
player.getContext().getResources().getDimension(R.dimen.popup_default_width);
final float popupWidth = popupRememberSizeAndPos
? player.getPrefs().getFloat(player.getContext().getString(
- R.string.popup_saved_width_key), defaultSize)
+ R.string.popup_saved_width_key), defaultSize)
: defaultSize;
final float popupHeight = getMinimumVideoHeight(popupWidth);
@@ -564,10 +607,10 @@ public static WindowManager.LayoutParams retrievePopupLayoutParamsFromPrefs(
final int centerY = (int) (player.getScreenHeight() / 2f - popupHeight / 2f);
popupLayoutParams.x = popupRememberSizeAndPos
? player.getPrefs().getInt(player.getContext().getString(
- R.string.popup_saved_x_key), centerX) : centerX;
+ R.string.popup_saved_x_key), centerX) : centerX;
popupLayoutParams.y = popupRememberSizeAndPos
? player.getPrefs().getInt(player.getContext().getString(
- R.string.popup_saved_y_key), centerY) : centerY;
+ R.string.popup_saved_y_key), centerY) : centerY;
return popupLayoutParams;
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/listeners/view/QualityClickListener.kt b/app/src/main/java/org/schabi/newpipe/player/listeners/view/QualityClickListener.kt
index b103ac0e6c5..43e8288e605 100644
--- a/app/src/main/java/org/schabi/newpipe/player/listeners/view/QualityClickListener.kt
+++ b/app/src/main/java/org/schabi/newpipe/player/listeners/view/QualityClickListener.kt
@@ -32,7 +32,7 @@ class QualityClickListener(
val videoStream = player.selectedVideoStream
if (videoStream != null) {
player.binding.qualityTextView.text =
- MediaFormat.getNameById(videoStream.formatId) + " " + videoStream.resolution
+ MediaFormat.getNameById(videoStream.formatId) + " " + videoStream.getResolution()
}
player.saveWasPlaying()
diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java
index 9bded9331c7..765475b2faa 100644
--- a/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java
+++ b/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java
@@ -1,13 +1,15 @@
package org.schabi.newpipe.player.resolver;
+import static org.schabi.newpipe.util.ListHelper.removeTorrentStreams;
+
import android.content.Context;
+import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.source.MediaSource;
-import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.player.helper.PlayerDataSource;
@@ -16,7 +18,13 @@
import org.schabi.newpipe.player.mediaitem.StreamInfoTag;
import org.schabi.newpipe.util.ListHelper;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
public class AudioPlaybackResolver implements PlaybackResolver {
+ private static final String TAG = AudioPlaybackResolver.class.getSimpleName();
+
@NonNull
private final Context context;
@NonNull
@@ -31,19 +39,28 @@ public AudioPlaybackResolver(@NonNull final Context context,
@Override
@Nullable
public MediaSource resolve(@NonNull final StreamInfo info) {
- final MediaSource liveSource = maybeBuildLiveMediaSource(dataSource, info);
+ final MediaSource liveSource = PlaybackResolver.maybeBuildLiveMediaSource(dataSource, info);
if (liveSource != null) {
return liveSource;
}
- final int index = ListHelper.getDefaultAudioFormat(context, info.getAudioStreams());
+ final List audioStreams = new ArrayList<>(info.getAudioStreams());
+ removeTorrentStreams(audioStreams);
+
+ final int index = ListHelper.getDefaultAudioFormat(context, audioStreams);
if (index < 0 || index >= info.getAudioStreams().size()) {
return null;
}
final AudioStream audio = info.getAudioStreams().get(index);
final MediaItemTag tag = StreamInfoTag.of(info);
- return buildMediaSource(dataSource, audio.getUrl(), PlayerHelper.cacheKeyOf(info, audio),
- MediaFormat.getSuffixById(audio.getFormatId()), tag);
+
+ try {
+ return PlaybackResolver.buildMediaSource(
+ dataSource, audio, info, PlayerHelper.cacheKeyOf(info, audio), tag);
+ } catch (final IOException e) {
+ Log.e(TAG, "Unable to create audio source:", e);
+ return null;
+ }
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java
index 90b38ed51da..4c1b67dfc6f 100644
--- a/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java
+++ b/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java
@@ -1,15 +1,38 @@
package org.schabi.newpipe.player.resolver;
+import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
+import static org.schabi.newpipe.player.helper.PlayerDataSource.LIVE_STREAM_EDGE_GAP_MILLIS;
+
import android.net.Uri;
-import android.text.TextUtils;
+import android.util.Log;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.source.MediaSource;
-import com.google.android.exoplayer2.util.Util;
+import com.google.android.exoplayer2.source.ProgressiveMediaSource;
+import com.google.android.exoplayer2.source.dash.DashMediaSource;
+import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
+import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser;
+import com.google.android.exoplayer2.source.hls.HlsMediaSource;
+import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist;
+import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser;
+import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
+import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest;
+import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser;
+import org.schabi.newpipe.extractor.ServiceList;
+import org.schabi.newpipe.extractor.services.youtube.ItagItem;
+import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.CreationException;
+import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeOtfDashManifestCreator;
+import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubePostLiveStreamDvrDashManifestCreator;
+import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeProgressiveDashManifestCreator;
+import org.schabi.newpipe.extractor.stream.AudioStream;
+import org.schabi.newpipe.extractor.stream.DeliveryMethod;
+import org.schabi.newpipe.extractor.stream.Stream;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamType;
+import org.schabi.newpipe.extractor.stream.VideoStream;
+import org.schabi.newpipe.player.helper.NonUriHlsPlaylistParserFactory;
import org.schabi.newpipe.player.helper.PlayerDataSource;
import org.schabi.newpipe.player.mediaitem.MediaItemTag;
import org.schabi.newpipe.player.mediaitem.StreamInfoTag;
@@ -18,13 +41,17 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import static org.schabi.newpipe.player.helper.PlayerDataSource.LIVE_STREAM_EDGE_GAP_MILLIS;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Objects;
public interface PlaybackResolver extends Resolver {
+ String TAG = PlaybackResolver.class.getSimpleName();
@Nullable
- default MediaSource maybeBuildLiveMediaSource(@NonNull final PlayerDataSource dataSource,
- @NonNull final StreamInfo info) {
+ static MediaSource maybeBuildLiveMediaSource(@NonNull final PlayerDataSource dataSource,
+ @NonNull final StreamInfo info) {
final StreamType streamType = info.getStreamType();
if (!StreamTypeUtil.isLiveStream(streamType)) {
return null;
@@ -41,10 +68,10 @@ default MediaSource maybeBuildLiveMediaSource(@NonNull final PlayerDataSource da
}
@NonNull
- default MediaSource buildLiveMediaSource(@NonNull final PlayerDataSource dataSource,
- @NonNull final String sourceUrl,
- @C.ContentType final int type,
- @NonNull final MediaItemTag metadata) {
+ static MediaSource buildLiveMediaSource(@NonNull final PlayerDataSource dataSource,
+ @NonNull final String sourceUrl,
+ @C.ContentType final int type,
+ @NonNull final MediaItemTag metadata) {
final MediaSource.Factory factory;
switch (type) {
case C.TYPE_SS:
@@ -67,46 +94,342 @@ default MediaSource buildLiveMediaSource(@NonNull final PlayerDataSource dataSou
.setLiveConfiguration(
new MediaItem.LiveConfiguration.Builder()
.setTargetOffsetMs(LIVE_STREAM_EDGE_GAP_MILLIS)
- .build()
- )
- .build()
- );
+ .build())
+ .build());
}
@NonNull
- default MediaSource buildMediaSource(@NonNull final PlayerDataSource dataSource,
- @NonNull final String sourceUrl,
- @NonNull final String cacheKey,
- @NonNull final String overrideExtension,
- @NonNull final MediaItemTag metadata) {
- final Uri uri = Uri.parse(sourceUrl);
- @C.ContentType final int type = TextUtils.isEmpty(overrideExtension)
- ? Util.inferContentType(uri) : Util.inferContentType("." + overrideExtension);
+ static MediaSource buildMediaSource(@NonNull final PlayerDataSource dataSource,
+ @NonNull final Stream stream,
+ @NonNull final StreamInfo streamInfo,
+ @NonNull final String cacheKey,
+ @NonNull final MediaItemTag metadata)
+ throws IOException {
+ if (streamInfo.getService() == ServiceList.YouTube) {
+ return createYoutubeMediaSource(stream, streamInfo, dataSource, cacheKey, metadata);
+ }
- final MediaSource.Factory factory;
- switch (type) {
- case C.TYPE_SS:
- factory = dataSource.getLiveSsMediaSourceFactory();
- break;
- case C.TYPE_DASH:
- factory = dataSource.getDashMediaSourceFactory();
- break;
- case C.TYPE_HLS:
- factory = dataSource.getHlsMediaSourceFactory();
- break;
- case C.TYPE_OTHER:
- factory = dataSource.getExtractorMediaSourceFactory();
- break;
+ final DeliveryMethod deliveryMethod = stream.getDeliveryMethod();
+ switch (deliveryMethod) {
+ case PROGRESSIVE_HTTP:
+ return buildProgressiveMediaSource(dataSource, stream, cacheKey, metadata);
+ case DASH:
+ return buildDashMediaSource(dataSource, stream, cacheKey, metadata);
+ case HLS:
+ return buildHlsMediaSource(dataSource, stream, cacheKey, metadata);
+ case SS:
+ return buildSSMediaSource(dataSource, stream, cacheKey, metadata);
+ // Torrent streams are not supported by ExoPlayer
default:
- throw new IllegalStateException("Unsupported type: " + type);
+ throw new IllegalArgumentException("Unsupported delivery type: " + deliveryMethod);
}
+ }
- return factory.createMediaSource(
+ @NonNull
+ private static ProgressiveMediaSource buildProgressiveMediaSource(
+ @NonNull final PlayerDataSource dataSource,
+ @NonNull final T stream,
+ @NonNull final String cacheKey,
+ @NonNull final MediaItemTag metadata) throws IOException {
+ final String url = stream.getContent();
+
+ if (isNullOrEmpty(url)) {
+ throw new IOException(
+ "Try to generate a progressive media source from an empty string or from a "
+ + "null object");
+ } else {
+ return dataSource.getProgressiveMediaSourceFactory().createMediaSource(
+ new MediaItem.Builder()
+ .setTag(metadata)
+ .setUri(Uri.parse(url))
+ .setCustomCacheKey(cacheKey)
+ .build());
+ }
+ }
+
+ @NonNull
+ private static DashMediaSource buildDashMediaSource(
+ @NonNull final PlayerDataSource dataSource,
+ @NonNull final T stream,
+ @NonNull final String cacheKey,
+ @NonNull final MediaItemTag metadata) throws IOException {
+ final boolean isUrlStream = stream.isUrl();
+ if (isUrlStream && isNullOrEmpty(stream.getContent())) {
+ throw new IOException("Try to generate a DASH media source from an empty string or "
+ + "from a null object");
+ }
+
+ if (isUrlStream) {
+ return dataSource.getDashMediaSourceFactory().createMediaSource(
+ new MediaItem.Builder()
+ .setTag(metadata)
+ .setUri(Uri.parse(stream.getContent()))
+ .setCustomCacheKey(cacheKey)
+ .build());
+ } else {
+ String baseUrl = stream.getManifestUrl();
+ if (baseUrl == null) {
+ baseUrl = "";
+ }
+
+ final Uri uri = Uri.parse(baseUrl);
+
+ return dataSource.getDashMediaSourceFactory().createMediaSource(
+ createDashManifest(stream.getContent(), stream),
+ new MediaItem.Builder()
+ .setTag(metadata)
+ .setUri(uri)
+ .setCustomCacheKey(cacheKey)
+ .build());
+ }
+ }
+
+ @NonNull
+ private static DashManifest createDashManifest(
+ @NonNull final String manifestContent,
+ @NonNull final T stream) throws IOException {
+ try {
+ final ByteArrayInputStream dashManifestInput = new ByteArrayInputStream(
+ manifestContent.getBytes(StandardCharsets.UTF_8));
+ String baseUrl = stream.getManifestUrl();
+ if (baseUrl == null) {
+ baseUrl = "";
+ }
+
+ return new DashManifestParser().parse(Uri.parse(baseUrl), dashManifestInput);
+ } catch (final IOException e) {
+ throw new IOException("Error when parsing manual DASH manifest", e);
+ }
+ }
+
+ @NonNull
+ private static HlsMediaSource buildHlsMediaSource(
+ @NonNull final PlayerDataSource dataSource,
+ @NonNull final T stream,
+ @NonNull final String cacheKey,
+ @NonNull final MediaItemTag metadata) throws IOException {
+ final boolean isUrlStream = stream.isUrl();
+ if (isUrlStream && isNullOrEmpty(stream.getContent())) {
+ throw new IOException("Try to generate an HLS media source from an empty string or "
+ + "from a null object");
+ }
+
+ if (isUrlStream) {
+ return dataSource.getHlsMediaSourceFactory(null).createMediaSource(
+ new MediaItem.Builder()
+ .setTag(metadata)
+ .setUri(Uri.parse(stream.getContent()))
+ .setCustomCacheKey(cacheKey)
+ .build());
+ } else {
+ String baseUrl = stream.getManifestUrl();
+ if (baseUrl == null) {
+ baseUrl = "";
+ }
+
+ final Uri uri = Uri.parse(baseUrl);
+
+ final HlsPlaylist hlsPlaylist;
+ try {
+ final ByteArrayInputStream hlsManifestInput = new ByteArrayInputStream(
+ stream.getContent().getBytes(StandardCharsets.UTF_8));
+ hlsPlaylist = new HlsPlaylistParser().parse(uri, hlsManifestInput);
+ } catch (final IOException e) {
+ throw new IOException("Error when parsing manual HLS manifest", e);
+ }
+
+ return dataSource.getHlsMediaSourceFactory(
+ new NonUriHlsPlaylistParserFactory(hlsPlaylist))
+ .createMediaSource(new MediaItem.Builder()
+ .setTag(metadata)
+ .setUri(Uri.parse(stream.getContent()))
+ .setCustomCacheKey(cacheKey)
+ .build());
+ }
+ }
+
+ @NonNull
+ private static SsMediaSource buildSSMediaSource(
+ @NonNull final PlayerDataSource dataSource,
+ @NonNull final T stream,
+ @NonNull final String cacheKey,
+ @NonNull final MediaItemTag metadata) throws IOException {
+ final boolean isUrlStream = stream.isUrl();
+ if (isUrlStream && isNullOrEmpty(stream.getContent())) {
+ throw new IOException("Try to generate an SmoothStreaming media source from an empty "
+ + "string or from a null object");
+ }
+
+ if (isUrlStream) {
+ return dataSource.getSSMediaSourceFactory().createMediaSource(
+ new MediaItem.Builder()
+ .setTag(metadata)
+ .setUri(Uri.parse(stream.getContent()))
+ .setCustomCacheKey(cacheKey)
+ .build());
+ } else {
+ String baseUrl = stream.getManifestUrl();
+ if (baseUrl == null) {
+ baseUrl = "";
+ }
+
+ final Uri uri = Uri.parse(baseUrl);
+
+ final SsManifest smoothStreamingManifest;
+ try {
+ final ByteArrayInputStream smoothStreamingManifestInput = new ByteArrayInputStream(
+ stream.getContent().getBytes(StandardCharsets.UTF_8));
+ smoothStreamingManifest = new SsManifestParser().parse(uri,
+ smoothStreamingManifestInput);
+ } catch (final IOException e) {
+ throw new IOException("Error when parsing manual SmoothStreaming manifest", e);
+ }
+
+ return dataSource.getSSMediaSourceFactory().createMediaSource(
+ smoothStreamingManifest,
+ new MediaItem.Builder()
+ .setTag(metadata)
+ .setUri(uri)
+ .setCustomCacheKey(cacheKey)
+ .build());
+ }
+ }
+
+ private static MediaSource createYoutubeMediaSource(
+ final T stream,
+ final StreamInfo streamInfo,
+ final PlayerDataSource dataSource,
+ final String cacheKey,
+ final MediaItemTag metadata) throws IOException {
+ if (!(stream instanceof AudioStream || stream instanceof VideoStream)) {
+ throw new IOException("Try to generate a DASH manifest of a YouTube "
+ + stream.getClass() + " " + stream.getContent());
+ }
+
+ final StreamType streamType = streamInfo.getStreamType();
+ if (streamType == StreamType.VIDEO_STREAM) {
+ return createYoutubeMediaSourceOfVideoStreamType(dataSource, stream, streamInfo,
+ cacheKey, metadata);
+ } else if (streamType == StreamType.POST_LIVE_STREAM) {
+ // If the content is not an URL, uses the DASH delivery method and if the stream type
+ // of the stream is a post live stream, it means that the content is an ended
+ // livestream so we need to generate the manifest corresponding to the content
+ // (which is the last segment of the stream)
+
+ try {
+ final ItagItem itagItem = Objects.requireNonNull(stream.getItagItem());
+ final String manifestString = YoutubePostLiveStreamDvrDashManifestCreator
+ .fromPostLiveStreamDvrStreamingUrl(stream.getContent(),
+ itagItem,
+ itagItem.getTargetDurationSec(),
+ streamInfo.getDuration());
+ return buildYoutubeManualDashMediaSource(dataSource,
+ createDashManifest(manifestString, stream), stream, cacheKey,
+ metadata);
+ } catch (final CreationException | NullPointerException e) {
+ Log.e(TAG, "Error when generating the DASH manifest of YouTube ended live stream",
+ e);
+ throw new IOException("Error when generating the DASH manifest of YouTube ended "
+ + "live stream " + stream.getContent(), e);
+ }
+ } else {
+ throw new IllegalArgumentException("DASH manifest generation of YouTube livestreams is "
+ + "not supported");
+ }
+ }
+
+ private static MediaSource createYoutubeMediaSourceOfVideoStreamType(
+ @NonNull final PlayerDataSource dataSource,
+ @NonNull final T stream,
+ @NonNull final StreamInfo streamInfo,
+ @NonNull final String cacheKey,
+ @NonNull final MediaItemTag metadata) throws IOException {
+ final DeliveryMethod deliveryMethod = stream.getDeliveryMethod();
+ switch (deliveryMethod) {
+ case PROGRESSIVE_HTTP:
+ if ((stream instanceof VideoStream && ((VideoStream) stream).isVideoOnly())
+ || stream instanceof AudioStream) {
+ try {
+ final String manifestString = YoutubeProgressiveDashManifestCreator
+ .fromProgressiveStreamingUrl(stream.getContent(),
+ Objects.requireNonNull(stream.getItagItem()),
+ streamInfo.getDuration());
+ return buildYoutubeManualDashMediaSource(dataSource,
+ createDashManifest(manifestString, stream), stream, cacheKey,
+ metadata);
+ } catch (final CreationException | IOException | NullPointerException e) {
+ Log.w(TAG, "Error when generating or parsing DASH manifest of "
+ + "YouTube progressive stream, falling back to a "
+ + "ProgressiveMediaSource.", e);
+ return buildYoutubeProgressiveMediaSource(dataSource, stream, cacheKey,
+ metadata);
+ }
+ } else {
+ // Legacy progressive streams, subtitles are handled by
+ // VideoPlaybackResolver
+ return buildYoutubeProgressiveMediaSource(dataSource, stream, cacheKey,
+ metadata);
+ }
+ case DASH:
+ // If the content is not a URL, uses the DASH delivery method and if the stream
+ // type of the stream is a video stream, it means the content is an OTF stream
+ // so we need to generate the manifest corresponding to the content (which is
+ // the base URL of the OTF stream).
+
+ try {
+ final String manifestString = YoutubeOtfDashManifestCreator
+ .fromOtfStreamingUrl(stream.getContent(),
+ Objects.requireNonNull(stream.getItagItem()),
+ streamInfo.getDuration());
+ return buildYoutubeManualDashMediaSource(dataSource,
+ createDashManifest(manifestString, stream), stream, cacheKey,
+ metadata);
+ } catch (final CreationException | NullPointerException e) {
+ Log.e(TAG,
+ "Error when generating the DASH manifest of YouTube OTF stream", e);
+ throw new IOException(
+ "Error when generating the DASH manifest of YouTube OTF stream "
+ + stream.getContent(), e);
+ }
+ case HLS:
+ return dataSource.getYoutubeHlsMediaSourceFactory().createMediaSource(
+ new MediaItem.Builder()
+ .setTag(metadata)
+ .setUri(Uri.parse(stream.getContent()))
+ .setCustomCacheKey(cacheKey)
+ .build());
+ default:
+ throw new IOException("Unsupported delivery method for YouTube contents: "
+ + deliveryMethod);
+ }
+ }
+
+ @NonNull
+ private static DashMediaSource buildYoutubeManualDashMediaSource(
+ @NonNull final PlayerDataSource dataSource,
+ @NonNull final DashManifest dashManifest,
+ @NonNull final T stream,
+ @NonNull final String cacheKey,
+ @NonNull final MediaItemTag metadata) {
+ return dataSource.getYoutubeDashMediaSourceFactory().createMediaSource(dashManifest,
new MediaItem.Builder()
- .setTag(metadata)
- .setUri(uri)
- .setCustomCacheKey(cacheKey)
- .build()
- );
+ .setTag(metadata)
+ .setUri(Uri.parse(stream.getContent()))
+ .setCustomCacheKey(cacheKey)
+ .build());
+ }
+
+ @NonNull
+ private static ProgressiveMediaSource buildYoutubeProgressiveMediaSource(
+ @NonNull final PlayerDataSource dataSource,
+ @NonNull final T stream,
+ @NonNull final String cacheKey,
+ @NonNull final MediaItemTag metadata) {
+ return dataSource.getYoutubeProgressiveMediaSourceFactory()
+ .createMediaSource(new MediaItem.Builder()
+ .setTag(metadata)
+ .setUri(Uri.parse(stream.getContent()))
+ .setCustomCacheKey(cacheKey)
+ .build());
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java
index 1aa7a5a18ab..24ca2e63a0e 100644
--- a/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java
+++ b/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java
@@ -2,6 +2,7 @@
import android.content.Context;
import android.net.Uri;
+import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -22,13 +23,18 @@
import org.schabi.newpipe.player.mediaitem.StreamInfoTag;
import org.schabi.newpipe.util.ListHelper;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import static com.google.android.exoplayer2.C.TIME_UNSET;
+import static org.schabi.newpipe.util.ListHelper.removeNonUrlAndTorrentStreams;
+import static org.schabi.newpipe.util.ListHelper.removeTorrentStreams;
public class VideoPlaybackResolver implements PlaybackResolver {
+ private static final String TAG = VideoPlaybackResolver.class.getSimpleName();
+
@NonNull
private final Context context;
@NonNull
@@ -57,17 +63,22 @@ public VideoPlaybackResolver(@NonNull final Context context,
@Override
@Nullable
public MediaSource resolve(@NonNull final StreamInfo info) {
- final MediaSource liveSource = maybeBuildLiveMediaSource(dataSource, info);
+ final MediaSource liveSource = PlaybackResolver.maybeBuildLiveMediaSource(dataSource, info);
if (liveSource != null) {
streamSourceType = SourceType.LIVE_STREAM;
return liveSource;
}
final List mediaSources = new ArrayList<>();
+ final List videoStreams = new ArrayList<>(info.getVideoStreams());
+ final List videoOnlyStreams = new ArrayList<>(info.getVideoOnlyStreams());
+
+ removeTorrentStreams(videoStreams);
+ removeTorrentStreams(videoOnlyStreams);
// Create video stream source
final List videos = ListHelper.getSortedStreamVideosList(context,
- info.getVideoStreams(), info.getVideoOnlyStreams(), false, true);
+ videoStreams, videoOnlyStreams, false, true);
final int index;
if (videos.isEmpty()) {
index = -1;
@@ -82,24 +93,34 @@ public MediaSource resolve(@NonNull final StreamInfo info) {
.orElse(null);
if (video != null) {
- final MediaSource streamSource = buildMediaSource(dataSource, video.getUrl(),
- PlayerHelper.cacheKeyOf(info, video),
- MediaFormat.getSuffixById(video.getFormatId()), tag);
- mediaSources.add(streamSource);
+ try {
+ final MediaSource streamSource = PlaybackResolver.buildMediaSource(
+ dataSource, video, info, PlayerHelper.cacheKeyOf(info, video), tag);
+ mediaSources.add(streamSource);
+ } catch (final IOException e) {
+ Log.e(TAG, "Unable to create video source:", e);
+ return null;
+ }
}
// Create optional audio stream source
final List audioStreams = info.getAudioStreams();
+ removeTorrentStreams(audioStreams);
final AudioStream audio = audioStreams.isEmpty() ? null : audioStreams.get(
ListHelper.getDefaultAudioFormat(context, audioStreams));
+
// Use the audio stream if there is no video stream, or
- // Merge with audio stream in case if video does not contain audio
- if (audio != null && (video == null || video.isVideoOnly)) {
- final MediaSource audioSource = buildMediaSource(dataSource, audio.getUrl(),
- PlayerHelper.cacheKeyOf(info, audio),
- MediaFormat.getSuffixById(audio.getFormatId()), tag);
- mediaSources.add(audioSource);
- streamSourceType = SourceType.VIDEO_WITH_SEPARATED_AUDIO;
+ // merge with audio stream in case if video does not contain audio
+ if (audio != null && (video == null || video.isVideoOnly())) {
+ try {
+ final MediaSource audioSource = PlaybackResolver.buildMediaSource(
+ dataSource, audio, info, PlayerHelper.cacheKeyOf(info, audio), tag);
+ mediaSources.add(audioSource);
+ streamSourceType = SourceType.VIDEO_WITH_SEPARATED_AUDIO;
+ } catch (final IOException e) {
+ Log.e(TAG, "Unable to create audio source:", e);
+ return null;
+ }
} else {
streamSourceType = SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY;
}
@@ -111,33 +132,35 @@ public MediaSource resolve(@NonNull final StreamInfo info) {
// Below are auxiliary media sources
// Create subtitle sources
- if (info.getSubtitles() != null) {
- for (final SubtitlesStream subtitle : info.getSubtitles()) {
- final String mimeType = PlayerHelper.subtitleMimeTypesOf(subtitle.getFormat());
- if (mimeType == null) {
- continue;
+ final List subtitlesStreams = info.getSubtitles();
+ if (subtitlesStreams != null) {
+ // Torrent and non URL subtitles are not supported by ExoPlayer
+ final List nonTorrentAndUrlStreams = removeNonUrlAndTorrentStreams(
+ subtitlesStreams);
+ for (final SubtitlesStream subtitle : nonTorrentAndUrlStreams) {
+ final MediaFormat mediaFormat = subtitle.getFormat();
+ if (mediaFormat != null) {
+ @C.RoleFlags final int textRoleFlag = subtitle.isAutoGenerated()
+ ? C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND
+ : C.ROLE_FLAG_CAPTION;
+ final MediaItem.SubtitleConfiguration textMediaItem =
+ new MediaItem.SubtitleConfiguration.Builder(
+ Uri.parse(subtitle.getContent()))
+ .setMimeType(mediaFormat.getMimeType())
+ .setRoleFlags(textRoleFlag)
+ .setLanguage(PlayerHelper.captionLanguageOf(context, subtitle))
+ .build();
+ final MediaSource textSource = dataSource.getSingleSampleMediaSourceFactory()
+ .createMediaSource(textMediaItem, TIME_UNSET);
+ mediaSources.add(textSource);
}
- final @C.RoleFlags int textRoleFlag = subtitle.isAutoGenerated()
- ? C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND
- : C.ROLE_FLAG_CAPTION;
- final MediaItem.SubtitleConfiguration textMediaItem =
- new MediaItem.SubtitleConfiguration.Builder(Uri.parse(subtitle.getUrl()))
- .setMimeType(mimeType)
- .setRoleFlags(textRoleFlag)
- .setLanguage(PlayerHelper.captionLanguageOf(context, subtitle))
- .build();
- final MediaSource textSource = dataSource
- .getSampleMediaSourceFactory()
- .createMediaSource(textMediaItem, TIME_UNSET);
- mediaSources.add(textSource);
}
}
if (mediaSources.size() == 1) {
return mediaSources.get(0);
} else {
- return new MergingMediaSource(mediaSources.toArray(
- new MediaSource[0]));
+ return new MergingMediaSource(true, mediaSources.toArray(new MediaSource[0]));
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java
index c3ccef87c59..3a03e0b3023 100644
--- a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java
@@ -13,6 +13,8 @@
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.stream.AudioStream;
+import org.schabi.newpipe.extractor.stream.DeliveryMethod;
+import org.schabi.newpipe.extractor.stream.Stream;
import org.schabi.newpipe.extractor.stream.VideoStream;
import java.util.ArrayList;
@@ -21,6 +23,7 @@
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Set;
@@ -37,10 +40,9 @@ public final class ListHelper {
// Audio format in order of efficiency. 0=most efficient, n=least efficient
private static final List AUDIO_FORMAT_EFFICIENCY_RANKING =
Arrays.asList(MediaFormat.WEBMA, MediaFormat.M4A, MediaFormat.MP3);
-
- private static final Set HIGH_RESOLUTION_LIST
- // Uses a HashSet for better performance
- = new HashSet<>(Arrays.asList("1440p", "2160p", "1440p60", "2160p60"));
+ // Use a HashSet for better performance
+ private static final Set HIGH_RESOLUTION_LIST = new HashSet<>(
+ Arrays.asList("1440p", "2160p"));
private ListHelper() { }
@@ -110,6 +112,83 @@ public static int getDefaultAudioFormat(final Context context,
}
}
+ /**
+ * Return a {@link Stream} list which uses the given delivery method from a {@link Stream}
+ * list.
+ *
+ * @param streamList the original stream list
+ * @param deliveryMethod the delivery method
+ * @param the item type's class that extends {@link Stream}
+ * @return a stream list which uses the given delivery method
+ */
+ @NonNull
+ public static List keepStreamsWithDelivery(
+ @NonNull final List streamList,
+ final DeliveryMethod deliveryMethod) {
+ if (streamList.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ final Iterator streamListIterator = streamList.iterator();
+ while (streamListIterator.hasNext()) {
+ if (streamListIterator.next().getDeliveryMethod() != deliveryMethod) {
+ streamListIterator.remove();
+ }
+ }
+
+ return streamList;
+ }
+
+ /**
+ * Return a {@link Stream} list which only contains URL streams and non-torrent streams.
+ *
+ * @param streamList the original stream list
+ * @param the item type's class that extends {@link Stream}
+ * @return a stream list which only contains URL streams and non-torrent streams
+ */
+ @NonNull
+ public static List removeNonUrlAndTorrentStreams(
+ @NonNull final List streamList) {
+ if (streamList.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ final Iterator streamListIterator = streamList.iterator();
+ while (streamListIterator.hasNext()) {
+ final S stream = streamListIterator.next();
+ if (!stream.isUrl() || stream.getDeliveryMethod() == DeliveryMethod.TORRENT) {
+ streamListIterator.remove();
+ }
+ }
+
+ return streamList;
+ }
+
+ /**
+ * Return a {@link Stream} list which only contains non-torrent streams.
+ *
+ * @param streamList the original stream list
+ * @param the item type's class that extends {@link Stream}
+ * @return a stream list which only contains non-torrent streams
+ */
+ @NonNull
+ public static List removeTorrentStreams(
+ @NonNull final List streamList) {
+ if (streamList.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ final Iterator streamListIterator = streamList.iterator();
+ while (streamListIterator.hasNext()) {
+ final S stream = streamListIterator.next();
+ if (stream.getDeliveryMethod() == DeliveryMethod.TORRENT) {
+ streamListIterator.remove();
+ }
+ }
+
+ return streamList;
+ }
+
/**
* Join the two lists of video streams (video_only and normal videos),
* and sort them according with default format chosen by the user.
@@ -177,7 +256,7 @@ private static String computeDefaultResolution(final Context context, final int
static int getDefaultResolutionIndex(final String defaultResolution,
final String bestResolutionKey,
final MediaFormat defaultFormat,
- final List videoStreams) {
+ @Nullable final List videoStreams) {
if (videoStreams == null || videoStreams.isEmpty()) {
return -1;
}
@@ -233,7 +312,9 @@ static List getSortedStreamVideosList(
.flatMap(List::stream)
// Filter out higher resolutions (or not if high resolutions should always be shown)
.filter(stream -> showHigherResolutions
- || !HIGH_RESOLUTION_LIST.contains(stream.getResolution()))
+ || !HIGH_RESOLUTION_LIST.contains(stream.getResolution()
+ // Replace any frame rate with nothing
+ .replaceAll("p\\d+$", "p")))
.collect(Collectors.toList());
final HashMap hashMap = new HashMap<>();
@@ -366,8 +447,9 @@ private static int getAudioIndexByHighestRank(@Nullable final MediaFormat target
* @param videoStreams the available video streams
* @return the index of the preferred video stream
*/
- static int getVideoStreamIndex(final String targetResolution, final MediaFormat targetFormat,
- final List videoStreams) {
+ static int getVideoStreamIndex(@NonNull final String targetResolution,
+ final MediaFormat targetFormat,
+ @NonNull final List videoStreams) {
int fullMatchIndex = -1;
int fullMatchNoRefreshIndex = -1;
int resMatchOnlyIndex = -1;
@@ -428,7 +510,7 @@ static int getVideoStreamIndex(final String targetResolution, final MediaFormat
* @param videoStreams the list of video streams to check
* @return the index of the preferred video stream
*/
- private static int getDefaultResolutionWithDefaultFormat(final Context context,
+ private static int getDefaultResolutionWithDefaultFormat(@NonNull final Context context,
final String defaultResolution,
final List videoStreams) {
final MediaFormat defaultFormat = getDefaultFormat(context,
@@ -437,7 +519,7 @@ private static int getDefaultResolutionWithDefaultFormat(final Context context,
context.getString(R.string.best_resolution_key), defaultFormat, videoStreams);
}
- private static MediaFormat getDefaultFormat(final Context context,
+ private static MediaFormat getDefaultFormat(@NonNull final Context context,
@StringRes final int defaultFormatKey,
@StringRes final int defaultFormatValueKey) {
final SharedPreferences preferences
@@ -457,8 +539,8 @@ private static MediaFormat getDefaultFormat(final Context context,
return defaultMediaFormat;
}
- private static MediaFormat getMediaFormatFromKey(final Context context,
- final String formatKey) {
+ private static MediaFormat getMediaFormatFromKey(@NonNull final Context context,
+ @NonNull final String formatKey) {
MediaFormat format = null;
if (formatKey.equals(context.getString(R.string.video_webm_key))) {
format = MediaFormat.WEBM;
@@ -496,12 +578,20 @@ private static int compareAudioStreamBitrate(final AudioStream streamA,
- formatRanking.indexOf(streamB.getFormat());
}
- private static int compareVideoStreamResolution(final String r1, final String r2) {
- final int res1 = Integer.parseInt(r1.replaceAll("0p\\d+$", "1")
- .replaceAll("[^\\d.]", ""));
- final int res2 = Integer.parseInt(r2.replaceAll("0p\\d+$", "1")
- .replaceAll("[^\\d.]", ""));
- return res1 - res2;
+ private static int compareVideoStreamResolution(@NonNull final String r1,
+ @NonNull final String r2) {
+ try {
+ final int res1 = Integer.parseInt(r1.replaceAll("0p\\d+$", "1")
+ .replaceAll("[^\\d.]", ""));
+ final int res2 = Integer.parseInt(r2.replaceAll("0p\\d+$", "1")
+ .replaceAll("[^\\d.]", ""));
+ return res1 - res2;
+ } catch (final NumberFormatException e) {
+ // Consider the first one greater because we don't know if the two streams are
+ // different or not (a NumberFormatException was thrown so we don't know the resolution
+ // of one stream or of all streams)
+ return 1;
+ }
}
// Compares the quality of two video streams.
@@ -536,7 +626,7 @@ private static boolean isLimitingDataUsage(final Context context) {
* @param context App context
* @return maximum resolution allowed or null if there is no maximum
*/
- private static String getResolutionLimit(final Context context) {
+ private static String getResolutionLimit(@NonNull final Context context) {
String resolutionLimit = null;
if (isMeteredNetwork(context)) {
final SharedPreferences preferences
@@ -555,7 +645,7 @@ private static String getResolutionLimit(final Context context) {
* @param context App context
* @return {@code true} if connected to a metered network
*/
- public static boolean isMeteredNetwork(final Context context) {
+ public static boolean isMeteredNetwork(@NonNull final Context context) {
final ConnectivityManager manager
= ContextCompat.getSystemService(context, ConnectivityManager.class);
if (manager == null || manager.getActiveNetworkInfo() == null) {
diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java
index e55114a2dd4..c3246857e1c 100644
--- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java
@@ -33,6 +33,7 @@
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.stream.AudioStream;
+import org.schabi.newpipe.extractor.stream.DeliveryMethod;
import org.schabi.newpipe.extractor.stream.Stream;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
@@ -60,7 +61,9 @@
import org.schabi.newpipe.settings.SettingsActivity;
import org.schabi.newpipe.util.external_communication.ShareUtils;
-import java.util.ArrayList;
+import java.util.List;
+
+import static org.schabi.newpipe.util.ListHelper.removeNonUrlAndTorrentStreams;
public final class NavigationHelper {
public static final String MAIN_FRAGMENT_TAG = "main_fragment_tag";
@@ -217,30 +220,44 @@ public static void enqueueNextOnPlayer(final Context context, final PlayQueue qu
public static void playOnExternalAudioPlayer(@NonNull final Context context,
@NonNull final StreamInfo info) {
- final int index = ListHelper.getDefaultAudioFormat(context, info.getAudioStreams());
-
- if (index == -1) {
+ final List audioStreams = info.getAudioStreams();
+ if (audioStreams.isEmpty()) {
Toast.makeText(context, R.string.audio_streams_empty, Toast.LENGTH_SHORT).show();
return;
}
+ final List audioStreamsForExternalPlayers = removeNonUrlAndTorrentStreams(
+ audioStreams);
+ if (audioStreamsForExternalPlayers.isEmpty()) {
+ Toast.makeText(context, R.string.no_audio_streams_available_for_external_players,
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+ final int index = ListHelper.getDefaultAudioFormat(context,
+ audioStreamsForExternalPlayers);
- final AudioStream audioStream = info.getAudioStreams().get(index);
+ final AudioStream audioStream = audioStreamsForExternalPlayers.get(index);
playOnExternalPlayer(context, info.getName(), info.getUploaderName(), audioStream);
}
- public static void playOnExternalVideoPlayer(@NonNull final Context context,
+ public static void playOnExternalVideoPlayer(final Context context,
@NonNull final StreamInfo info) {
- final ArrayList videoStreamsList = new ArrayList<>(
- ListHelper.getSortedStreamVideosList(context, info.getVideoStreams(), null, false,
- false));
- final int index = ListHelper.getDefaultResolutionIndex(context, videoStreamsList);
-
- if (index == -1) {
+ final List videoStreams = info.getVideoStreams();
+ if (videoStreams.isEmpty()) {
Toast.makeText(context, R.string.video_streams_empty, Toast.LENGTH_SHORT).show();
return;
}
+ final List videoStreamsForExternalPlayers =
+ ListHelper.getSortedStreamVideosList(context,
+ removeNonUrlAndTorrentStreams(videoStreams), null, false, false);
+ if (videoStreamsForExternalPlayers.isEmpty()) {
+ Toast.makeText(context, R.string.no_video_streams_available_for_external_players,
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+ final int index = ListHelper.getDefaultResolutionIndex(context,
+ videoStreamsForExternalPlayers);
- final VideoStream videoStream = videoStreamsList.get(index);
+ final VideoStream videoStream = videoStreamsForExternalPlayers.get(index);
playOnExternalPlayer(context, info.getName(), info.getUploaderName(), videoStream);
}
@@ -248,9 +265,49 @@ public static void playOnExternalPlayer(@NonNull final Context context,
@Nullable final String name,
@Nullable final String artist,
@NonNull final Stream stream) {
+ final DeliveryMethod deliveryMethod = stream.getDeliveryMethod();
+ final String mimeType;
+ if (deliveryMethod == DeliveryMethod.PROGRESSIVE_HTTP) {
+ if (stream.getFormat() != null) {
+ mimeType = stream.getFormat().getMimeType();
+ } else {
+ if (stream instanceof AudioStream) {
+ mimeType = "audio/*";
+ } else if (stream instanceof VideoStream) {
+ mimeType = "video/*";
+ } else {
+ // This should never be reached, because subtitles are not opened in external
+ // players
+ return;
+ }
+ }
+ } else {
+ if (!stream.isUrl() || deliveryMethod == DeliveryMethod.TORRENT) {
+ Toast.makeText(context, R.string.selected_stream_external_player_not_supported,
+ Toast.LENGTH_SHORT).show();
+ return;
+ } else {
+ switch (deliveryMethod) {
+ case HLS:
+ mimeType = "application/x-mpegURL";
+ break;
+ case DASH:
+ mimeType = "application/dash+xml";
+ break;
+ case SS:
+ mimeType = "application/vnd.ms-sstr+xml";
+ break;
+ default:
+ // Progressive HTTP streams are handled above and torrents streams are not
+ // exposed to external players
+ mimeType = "";
+ }
+ }
+ }
+
final Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
- intent.setDataAndType(Uri.parse(stream.getUrl()), stream.getFormat().getMimeType());
+ intent.setDataAndType(Uri.parse(stream.getContent()), mimeType);
intent.putExtra(Intent.EXTRA_TITLE, name);
intent.putExtra("title", name);
intent.putExtra("artist", artist);
diff --git a/app/src/main/java/org/schabi/newpipe/util/SecondaryStreamHelper.java b/app/src/main/java/org/schabi/newpipe/util/SecondaryStreamHelper.java
index 8c697d32730..96124da8744 100644
--- a/app/src/main/java/org/schabi/newpipe/util/SecondaryStreamHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/util/SecondaryStreamHelper.java
@@ -1,6 +1,7 @@
package org.schabi.newpipe.util;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.stream.AudioStream;
@@ -14,7 +15,8 @@ public class SecondaryStreamHelper {
private final int position;
private final StreamSizeWrapper streams;
- public SecondaryStreamHelper(final StreamSizeWrapper streams, final T selectedStream) {
+ public SecondaryStreamHelper(@NonNull final StreamSizeWrapper streams,
+ final T selectedStream) {
this.streams = streams;
this.position = streams.getStreamsList().indexOf(selectedStream);
if (this.position < 0) {
@@ -29,33 +31,37 @@ public SecondaryStreamHelper(final StreamSizeWrapper streams, final T selecte
* @param videoStream desired video ONLY stream
* @return selected audio stream or null if a candidate was not found
*/
+ @Nullable
public static AudioStream getAudioStreamFor(@NonNull final List audioStreams,
@NonNull final VideoStream videoStream) {
- switch (videoStream.getFormat()) {
- case WEBM:
- case MPEG_4:// ¿is mpeg-4 DASH?
- break;
- default:
- return null;
- }
+ final MediaFormat mediaFormat = videoStream.getFormat();
+ if (mediaFormat != null) {
+ switch (mediaFormat) {
+ case WEBM:
+ case MPEG_4:// ¿is mpeg-4 DASH?
+ break;
+ default:
+ return null;
+ }
- final boolean m4v = videoStream.getFormat() == MediaFormat.MPEG_4;
+ final boolean m4v = (mediaFormat == MediaFormat.MPEG_4);
- for (final AudioStream audio : audioStreams) {
- if (audio.getFormat() == (m4v ? MediaFormat.M4A : MediaFormat.WEBMA)) {
- return audio;
+ for (final AudioStream audio : audioStreams) {
+ if (audio.getFormat() == (m4v ? MediaFormat.M4A : MediaFormat.WEBMA)) {
+ return audio;
+ }
}
- }
- if (m4v) {
- return null;
- }
+ if (m4v) {
+ return null;
+ }
- // retry, but this time in reverse order
- for (int i = audioStreams.size() - 1; i >= 0; i--) {
- final AudioStream audio = audioStreams.get(i);
- if (audio.getFormat() == MediaFormat.WEBMA_OPUS) {
- return audio;
+ // retry, but this time in reverse order
+ for (int i = audioStreams.size() - 1; i >= 0; i--) {
+ final AudioStream audio = audioStreams.get(i);
+ if (audio.getFormat() == MediaFormat.WEBMA_OPUS) {
+ return audio;
+ }
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java b/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java
index 03342a49770..11f982921fc 100644
--- a/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java
+++ b/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java
@@ -10,6 +10,8 @@
import android.widget.Spinner;
import android.widget.TextView;
+import androidx.annotation.NonNull;
+
import org.schabi.newpipe.DownloaderImpl;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.MediaFormat;
@@ -87,7 +89,8 @@ public long getItemId(final int position) {
}
@Override
- public View getDropDownView(final int position, final View convertView,
+ public View getDropDownView(final int position,
+ final View convertView,
final ViewGroup parent) {
return getCustomView(position, convertView, parent, true);
}
@@ -98,7 +101,10 @@ public View getView(final int position, final View convertView, final ViewGroup
convertView, parent, false);
}
- private View getCustomView(final int position, final View view, final ViewGroup parent,
+ @NonNull
+ private View getCustomView(final int position,
+ final View view,
+ final ViewGroup parent,
final boolean isDropdownItem) {
View convertView = view;
if (convertView == null) {
@@ -112,6 +118,7 @@ private View getCustomView(final int position, final View view, final ViewGroup
final TextView sizeView = convertView.findViewById(R.id.stream_size);
final T stream = getItem(position);
+ final MediaFormat mediaFormat = stream.getFormat();
int woSoundIconVisibility = View.GONE;
String qualityString;
@@ -135,24 +142,32 @@ private View getCustomView(final int position, final View view, final ViewGroup
}
} else if (stream instanceof AudioStream) {
final AudioStream audioStream = ((AudioStream) stream);
- qualityString = audioStream.getAverageBitrate() > 0
- ? audioStream.getAverageBitrate() + "kbps"
- : audioStream.getFormat().getName();
+ if (audioStream.getAverageBitrate() > 0) {
+ qualityString = audioStream.getAverageBitrate() + "kbps";
+ } else if (mediaFormat != null) {
+ qualityString = mediaFormat.getName();
+ } else {
+ qualityString = context.getString(R.string.unknown_quality);
+ }
} else if (stream instanceof SubtitlesStream) {
qualityString = ((SubtitlesStream) stream).getDisplayLanguageName();
if (((SubtitlesStream) stream).isAutoGenerated()) {
qualityString += " (" + context.getString(R.string.caption_auto_generated) + ")";
}
} else {
- qualityString = stream.getFormat().getSuffix();
+ if (mediaFormat != null) {
+ qualityString = mediaFormat.getSuffix();
+ } else {
+ qualityString = context.getString(R.string.unknown_quality);
+ }
}
if (streamsWrapper.getSizeInBytes(position) > 0) {
final SecondaryStreamHelper secondary = secondaryStreams == null ? null
: secondaryStreams.get(position);
if (secondary != null) {
- final long size
- = secondary.getSizeInBytes() + streamsWrapper.getSizeInBytes(position);
+ final long size = secondary.getSizeInBytes()
+ + streamsWrapper.getSizeInBytes(position);
sizeView.setText(Utility.formatBytes(size));
} else {
sizeView.setText(streamsWrapper.getFormattedSize(position));
@@ -164,11 +179,15 @@ private View getCustomView(final int position, final View view, final ViewGroup
if (stream instanceof SubtitlesStream) {
formatNameView.setText(((SubtitlesStream) stream).getLanguageTag());
- } else if (stream.getFormat() == MediaFormat.WEBMA_OPUS) {
- // noinspection AndroidLintSetTextI18n
- formatNameView.setText("opus");
} else {
- formatNameView.setText(stream.getFormat().getName());
+ if (mediaFormat == null) {
+ formatNameView.setText(context.getString(R.string.unknown_format));
+ } else if (mediaFormat == MediaFormat.WEBMA_OPUS) {
+ // noinspection AndroidLintSetTextI18n
+ formatNameView.setText("opus");
+ } else {
+ formatNameView.setText(mediaFormat.getName());
+ }
}
qualityView.setText(qualityString);
@@ -233,6 +252,7 @@ public StreamSizeWrapper(final List sL, final Context context) {
* @param streamsWrapper the wrapper
* @return a {@link Single} that returns a boolean indicating if any elements were changed
*/
+ @NonNull
public static Single fetchSizeForWrapper(
final StreamSizeWrapper streamsWrapper) {
final Callable fetchAndSet = () -> {
@@ -243,7 +263,7 @@ public static Single fetchSizeForWrapper(
}
final long contentLength = DownloaderImpl.getInstance().getContentLength(
- stream.getUrl());
+ stream.getContent());
streamsWrapper.setSize(stream, contentLength);
hasChanged = true;
}
diff --git a/app/src/main/java/org/schabi/newpipe/util/StreamTypeUtil.java b/app/src/main/java/org/schabi/newpipe/util/StreamTypeUtil.java
index 87b3eed4f13..b0b6f4507ef 100644
--- a/app/src/main/java/org/schabi/newpipe/util/StreamTypeUtil.java
+++ b/app/src/main/java/org/schabi/newpipe/util/StreamTypeUtil.java
@@ -3,7 +3,7 @@
import org.schabi.newpipe.extractor.stream.StreamType;
/**
- * Utility class for {@link org.schabi.newpipe.extractor.stream.StreamType}.
+ * Utility class for {@link StreamType}.
*/
public final class StreamTypeUtil {
private StreamTypeUtil() {
@@ -11,10 +11,10 @@ private StreamTypeUtil() {
}
/**
- * Checks if the streamType is a livestream.
+ * Check if the {@link StreamType} of a stream is a livestream.
*
- * @param streamType
- * @return true when the streamType is a
+ * @param streamType the stream type of the stream
+ * @return true if the streamType is a
* {@link StreamType#LIVE_STREAM} or {@link StreamType#AUDIO_LIVE_STREAM}
*/
public static boolean isLiveStream(final StreamType streamType) {
diff --git a/app/src/main/java/us/shandian/giga/get/DownloadMissionRecover.java b/app/src/main/java/us/shandian/giga/get/DownloadMissionRecover.java
index 90886b63c7b..e001c6f3fea 100644
--- a/app/src/main/java/us/shandian/giga/get/DownloadMissionRecover.java
+++ b/app/src/main/java/us/shandian/giga/get/DownloadMissionRecover.java
@@ -6,6 +6,7 @@
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.stream.AudioStream;
+import org.schabi.newpipe.extractor.stream.DeliveryMethod;
import org.schabi.newpipe.extractor.stream.StreamExtractor;
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
import org.schabi.newpipe.extractor.stream.VideoStream;
@@ -131,31 +132,38 @@ private void resolveStream() throws IOException, ExtractionException, HttpError
switch (mRecovery.getKind()) {
case 'a':
- for (AudioStream audio : mExtractor.getAudioStreams()) {
- if (audio.getAverageBitrate() == mRecovery.getDesiredBitrate() && audio.getFormat() == mRecovery.getFormat()) {
- url = audio.getUrl();
+ for (final AudioStream audio : mExtractor.getAudioStreams()) {
+ if (audio.getAverageBitrate() == mRecovery.getDesiredBitrate()
+ && audio.getFormat() == mRecovery.getFormat()
+ && audio.getDeliveryMethod() == DeliveryMethod.PROGRESSIVE_HTTP) {
+ url = audio.getContent();
break;
}
}
break;
case 'v':
- List videoStreams;
+ final List videoStreams;
if (mRecovery.isDesired2())
videoStreams = mExtractor.getVideoOnlyStreams();
else
videoStreams = mExtractor.getVideoStreams();
- for (VideoStream video : videoStreams) {
- if (video.resolution.equals(mRecovery.getDesired()) && video.getFormat() == mRecovery.getFormat()) {
- url = video.getUrl();
+ for (final VideoStream video : videoStreams) {
+ if (video.getResolution().equals(mRecovery.getDesired())
+ && video.getFormat() == mRecovery.getFormat()
+ && video.getDeliveryMethod() == DeliveryMethod.PROGRESSIVE_HTTP) {
+ url = video.getContent();
break;
}
}
break;
case 's':
- for (SubtitlesStream subtitles : mExtractor.getSubtitles(mRecovery.getFormat())) {
+ for (final SubtitlesStream subtitles : mExtractor.getSubtitles(mRecovery
+ .getFormat())) {
String tag = subtitles.getLanguageTag();
- if (tag.equals(mRecovery.getDesired()) && subtitles.isAutoGenerated() == mRecovery.isDesired2()) {
- url = subtitles.getUrl();
+ if (tag.equals(mRecovery.getDesired())
+ && subtitles.isAutoGenerated() == mRecovery.isDesired2()
+ && subtitles.getDeliveryMethod() == DeliveryMethod.PROGRESSIVE_HTTP) {
+ url = subtitles.getContent();
break;
}
}
diff --git a/app/src/main/java/us/shandian/giga/get/MissionRecoveryInfo.kt b/app/src/main/java/us/shandian/giga/get/MissionRecoveryInfo.kt
index 11293a61063..c2f9dc9b27a 100644
--- a/app/src/main/java/us/shandian/giga/get/MissionRecoveryInfo.kt
+++ b/app/src/main/java/us/shandian/giga/get/MissionRecoveryInfo.kt
@@ -11,23 +11,23 @@ import java.io.Serializable
@Parcelize
class MissionRecoveryInfo(
- var format: MediaFormat,
+ var format: MediaFormat?,
var desired: String? = null,
var isDesired2: Boolean = false,
var desiredBitrate: Int = 0,
var kind: Char = Char.MIN_VALUE,
var validateCondition: String? = null
) : Serializable, Parcelable {
- constructor(stream: Stream) : this(format = stream.getFormat()!!) {
+ constructor(stream: Stream) : this(format = stream.format) {
when (stream) {
is AudioStream -> {
- desiredBitrate = stream.averageBitrate
+ desiredBitrate = stream.getAverageBitrate()
isDesired2 = false
kind = 'a'
}
is VideoStream -> {
- desired = stream.resolution
- isDesired2 = stream.isVideoOnly
+ desired = stream.getResolution()
+ isDesired2 = stream.isVideoOnly()
kind = 'v'
}
is SubtitlesStream -> {
@@ -62,7 +62,7 @@ class MissionRecoveryInfo(
}
}
str.append(" format=")
- .append(format.getName())
+ .append(format?.getName())
.append(' ')
.append(info)
.append('}')
diff --git a/app/src/main/res/layout/download_dialog.xml b/app/src/main/res/layout/download_dialog.xml
index 33e18c64a3f..4a9c0711f1b 100644
--- a/app/src/main/res/layout/download_dialog.xml
+++ b/app/src/main/res/layout/download_dialog.xml
@@ -82,6 +82,7 @@
android:text="@string/msg_threads" />
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 80f79cfdd97..1ab39d30270 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -740,4 +740,11 @@
You now subscribed to this channel,Toggle all
+ Note that streams which are not supported by the downloader yet have been removed
+ The selected stream is not supported by external players
+ No audio streams are available for external players
+ No video streams are available for external players
+ Select quality for external players
+ Unknown format
+ Unknown quality
\ No newline at end of file
diff --git a/app/src/test/java/org/schabi/newpipe/util/ListHelperTest.java b/app/src/test/java/org/schabi/newpipe/util/ListHelperTest.java
index 531837ea21d..c9d570c7d4d 100644
--- a/app/src/test/java/org/schabi/newpipe/util/ListHelperTest.java
+++ b/app/src/test/java/org/schabi/newpipe/util/ListHelperTest.java
@@ -13,38 +13,41 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
public class ListHelperTest {
private static final String BEST_RESOLUTION_KEY = "best_resolution";
private static final List AUDIO_STREAMS_TEST_LIST = Arrays.asList(
- new AudioStream("", MediaFormat.M4A, /**/ 128),
- new AudioStream("", MediaFormat.WEBMA, /**/ 192),
- new AudioStream("", MediaFormat.MP3, /**/ 64),
- new AudioStream("", MediaFormat.WEBMA, /**/ 192),
- new AudioStream("", MediaFormat.M4A, /**/ 128),
- new AudioStream("", MediaFormat.MP3, /**/ 128),
- new AudioStream("", MediaFormat.WEBMA, /**/ 64),
- new AudioStream("", MediaFormat.M4A, /**/ 320),
- new AudioStream("", MediaFormat.MP3, /**/ 192),
- new AudioStream("", MediaFormat.WEBMA, /**/ 320));
+ generateAudioStream("m4a-128-1", MediaFormat.M4A, 128),
+ generateAudioStream("webma-192", MediaFormat.WEBMA, 192),
+ generateAudioStream("mp3-64", MediaFormat.MP3, 64),
+ generateAudioStream("webma-192", MediaFormat.WEBMA, 192),
+ generateAudioStream("m4a-128-2", MediaFormat.M4A, 128),
+ generateAudioStream("mp3-128", MediaFormat.MP3, 128),
+ generateAudioStream("webma-64", MediaFormat.WEBMA, 64),
+ generateAudioStream("m4a-320", MediaFormat.M4A, 320),
+ generateAudioStream("mp3-192", MediaFormat.MP3, 192),
+ generateAudioStream("webma-320", MediaFormat.WEBMA, 320));
private static final List VIDEO_STREAMS_TEST_LIST = Arrays.asList(
- new VideoStream("", MediaFormat.MPEG_4, /**/ "720p"),
- new VideoStream("", MediaFormat.v3GPP, /**/ "240p"),
- new VideoStream("", MediaFormat.WEBM, /**/ "480p"),
- new VideoStream("", MediaFormat.v3GPP, /**/ "144p"),
- new VideoStream("", MediaFormat.MPEG_4, /**/ "360p"),
- new VideoStream("", MediaFormat.WEBM, /**/ "360p"));
+ generateVideoStream("mpeg_4-720", MediaFormat.MPEG_4, "720p", false),
+ generateVideoStream("v3gpp-240", MediaFormat.v3GPP, "240p", false),
+ generateVideoStream("webm-480", MediaFormat.WEBM, "480p", false),
+ generateVideoStream("v3gpp-144", MediaFormat.v3GPP, "144p", false),
+ generateVideoStream("mpeg_4-360", MediaFormat.MPEG_4, "360p", false),
+ generateVideoStream("webm-360", MediaFormat.WEBM, "360p", false));
private static final List VIDEO_ONLY_STREAMS_TEST_LIST = Arrays.asList(
- new VideoStream("", MediaFormat.MPEG_4, /**/ "720p", true),
- new VideoStream("", MediaFormat.MPEG_4, /**/ "720p", true),
- new VideoStream("", MediaFormat.MPEG_4, /**/ "2160p", true),
- new VideoStream("", MediaFormat.MPEG_4, /**/ "1440p60", true),
- new VideoStream("", MediaFormat.WEBM, /**/ "720p60", true),
- new VideoStream("", MediaFormat.MPEG_4, /**/ "2160p60", true),
- new VideoStream("", MediaFormat.MPEG_4, /**/ "720p60", true),
- new VideoStream("", MediaFormat.MPEG_4, /**/ "1080p", true),
- new VideoStream("", MediaFormat.MPEG_4, /**/ "1080p60", true));
+ generateVideoStream("mpeg_4-720-1", MediaFormat.MPEG_4, "720p", true),
+ generateVideoStream("mpeg_4-720-2", MediaFormat.MPEG_4, "720p", true),
+ generateVideoStream("mpeg_4-2160", MediaFormat.MPEG_4, "2160p", true),
+ generateVideoStream("mpeg_4-1440_60", MediaFormat.MPEG_4, "1440p60", true),
+ generateVideoStream("webm-720_60", MediaFormat.WEBM, "720p60", true),
+ generateVideoStream("mpeg_4-2160_60", MediaFormat.MPEG_4, "2160p60", true),
+ generateVideoStream("mpeg_4-720_60", MediaFormat.MPEG_4, "720p60", true),
+ generateVideoStream("mpeg_4-1080", MediaFormat.MPEG_4, "1080p", true),
+ generateVideoStream("mpeg_4-1080_60", MediaFormat.MPEG_4, "1080p60", true));
@Test
public void getSortedStreamVideosListTest() {
@@ -56,7 +59,8 @@ public void getSortedStreamVideosListTest() {
assertEquals(expected.size(), result.size());
for (int i = 0; i < result.size(); i++) {
- assertEquals(expected.get(i), result.get(i).resolution);
+ assertEquals(result.get(i).getResolution(), expected.get(i));
+ assertEquals(expected.get(i), result.get(i).getResolution());
}
////////////////////
@@ -69,7 +73,7 @@ public void getSortedStreamVideosListTest() {
"720p", "480p", "360p", "240p", "144p");
assertEquals(expected.size(), result.size());
for (int i = 0; i < result.size(); i++) {
- assertEquals(expected.get(i), result.get(i).resolution);
+ assertEquals(expected.get(i), result.get(i).getResolution());
}
}
@@ -83,8 +87,8 @@ public void getSortedStreamVideosListWithPreferVideoOnlyStreamsTest() {
assertEquals(expected.size(), result.size());
for (int i = 0; i < result.size(); i++) {
- assertEquals(expected.get(i), result.get(i).resolution);
- assertTrue(result.get(i).isVideoOnly);
+ assertEquals(expected.get(i), result.get(i).getResolution());
+ assertTrue(result.get(i).isVideoOnly());
}
//////////////////////////////////////////////////////////
@@ -96,8 +100,8 @@ public void getSortedStreamVideosListWithPreferVideoOnlyStreamsTest() {
expected = Arrays.asList("720p", "480p", "360p", "240p", "144p");
assertEquals(expected.size(), result.size());
for (int i = 0; i < result.size(); i++) {
- assertEquals(expected.get(i), result.get(i).resolution);
- assertFalse(result.get(i).isVideoOnly);
+ assertEquals(expected.get(i), result.get(i).getResolution());
+ assertFalse(result.get(i).isVideoOnly());
}
/////////////////////////////////////////////////////////////////
@@ -113,10 +117,9 @@ public void getSortedStreamVideosListWithPreferVideoOnlyStreamsTest() {
assertEquals(expected.size(), result.size());
for (int i = 0; i < result.size(); i++) {
- assertEquals(expected.get(i), result.get(i).resolution);
- assertEquals(
- expectedVideoOnly.contains(result.get(i).resolution),
- result.get(i).isVideoOnly);
+ assertEquals(expected.get(i), result.get(i).getResolution());
+ assertEquals(expectedVideoOnly.contains(result.get(i).getResolution()),
+ result.get(i).isVideoOnly());
}
}
@@ -132,66 +135,66 @@ public void getSortedStreamVideosExceptHighResolutionsTest() {
"1080p60", "1080p", "720p60", "720p", "480p", "360p", "240p", "144p");
assertEquals(expected.size(), result.size());
for (int i = 0; i < result.size(); i++) {
- assertEquals(expected.get(i), result.get(i).resolution);
+ assertEquals(expected.get(i), result.get(i).getResolution());
}
}
@Test
public void getDefaultResolutionTest() {
final List testList = Arrays.asList(
- new VideoStream("", MediaFormat.MPEG_4, /**/ "720p"),
- new VideoStream("", MediaFormat.v3GPP, /**/ "240p"),
- new VideoStream("", MediaFormat.WEBM, /**/ "480p"),
- new VideoStream("", MediaFormat.WEBM, /**/ "240p"),
- new VideoStream("", MediaFormat.MPEG_4, /**/ "240p"),
- new VideoStream("", MediaFormat.WEBM, /**/ "144p"),
- new VideoStream("", MediaFormat.MPEG_4, /**/ "360p"),
- new VideoStream("", MediaFormat.WEBM, /**/ "360p"));
+ generateVideoStream("mpeg_4-720", MediaFormat.MPEG_4, "720p", false),
+ generateVideoStream("v3gpp-240", MediaFormat.v3GPP, "240p", false),
+ generateVideoStream("webm-480", MediaFormat.WEBM, "480p", false),
+ generateVideoStream("webm-240", MediaFormat.WEBM, "240p", false),
+ generateVideoStream("mpeg_4-240", MediaFormat.MPEG_4, "240p", false),
+ generateVideoStream("webm-144", MediaFormat.WEBM, "144p", false),
+ generateVideoStream("mpeg_4-360", MediaFormat.MPEG_4, "360p", false),
+ generateVideoStream("webm-360", MediaFormat.WEBM, "360p", false));
VideoStream result = testList.get(ListHelper.getDefaultResolutionIndex(
"720p", BEST_RESOLUTION_KEY, MediaFormat.MPEG_4, testList));
- assertEquals("720p", result.resolution);
+ assertEquals("720p", result.getResolution());
assertEquals(MediaFormat.MPEG_4, result.getFormat());
// Have resolution and the format
result = testList.get(ListHelper.getDefaultResolutionIndex(
"480p", BEST_RESOLUTION_KEY, MediaFormat.WEBM, testList));
- assertEquals("480p", result.resolution);
+ assertEquals("480p", result.getResolution());
assertEquals(MediaFormat.WEBM, result.getFormat());
// Have resolution but not the format
result = testList.get(ListHelper.getDefaultResolutionIndex(
"480p", BEST_RESOLUTION_KEY, MediaFormat.MPEG_4, testList));
- assertEquals("480p", result.resolution);
+ assertEquals("480p", result.getResolution());
assertEquals(MediaFormat.WEBM, result.getFormat());
// Have resolution and the format
result = testList.get(ListHelper.getDefaultResolutionIndex(
"240p", BEST_RESOLUTION_KEY, MediaFormat.WEBM, testList));
- assertEquals("240p", result.resolution);
+ assertEquals("240p", result.getResolution());
assertEquals(MediaFormat.WEBM, result.getFormat());
// The best resolution
result = testList.get(ListHelper.getDefaultResolutionIndex(
BEST_RESOLUTION_KEY, BEST_RESOLUTION_KEY, MediaFormat.WEBM, testList));
- assertEquals("720p", result.resolution);
+ assertEquals("720p", result.getResolution());
assertEquals(MediaFormat.MPEG_4, result.getFormat());
// Doesn't have the 60fps variant and format
result = testList.get(ListHelper.getDefaultResolutionIndex(
"720p60", BEST_RESOLUTION_KEY, MediaFormat.WEBM, testList));
- assertEquals("720p", result.resolution);
+ assertEquals("720p", result.getResolution());
assertEquals(MediaFormat.MPEG_4, result.getFormat());
// Doesn't have the 60fps variant
result = testList.get(ListHelper.getDefaultResolutionIndex(
"480p60", BEST_RESOLUTION_KEY, MediaFormat.WEBM, testList));
- assertEquals("480p", result.resolution);
+ assertEquals("480p", result.getResolution());
assertEquals(MediaFormat.WEBM, result.getFormat());
// Doesn't have the resolution, will return the best one
result = testList.get(ListHelper.getDefaultResolutionIndex(
"2160p60", BEST_RESOLUTION_KEY, MediaFormat.WEBM, testList));
- assertEquals("720p", result.resolution);
+ assertEquals("720p", result.getResolution());
assertEquals(MediaFormat.MPEG_4, result.getFormat());
}
@@ -221,8 +224,8 @@ public void getHighestQualityAudioFormatPreferredAbsent() {
////////////////////////////////////////
List testList = Arrays.asList(
- new AudioStream("", MediaFormat.M4A, /**/ 128),
- new AudioStream("", MediaFormat.WEBMA, /**/ 192));
+ generateAudioStream("m4a-128", MediaFormat.M4A, 128),
+ generateAudioStream("webma-192", MediaFormat.WEBMA, 192));
// List doesn't contains this format
// It should fallback to the highest bitrate audio no matter what format it is
AudioStream stream = testList.get(ListHelper.getHighestQualityAudioIndex(
@@ -235,13 +238,13 @@ public void getHighestQualityAudioFormatPreferredAbsent() {
//////////////////////////////////////////////////////
testList = new ArrayList<>(Arrays.asList(
- new AudioStream("", MediaFormat.WEBMA, /**/ 192),
- new AudioStream("", MediaFormat.M4A, /**/ 192),
- new AudioStream("", MediaFormat.WEBMA, /**/ 192),
- new AudioStream("", MediaFormat.M4A, /**/ 192),
- new AudioStream("", MediaFormat.WEBMA, /**/ 192),
- new AudioStream("", MediaFormat.M4A, /**/ 192),
- new AudioStream("", MediaFormat.WEBMA, /**/ 192)));
+ generateAudioStream("webma-192-1", MediaFormat.WEBMA, 192),
+ generateAudioStream("m4a-192-1", MediaFormat.M4A, 192),
+ generateAudioStream("webma-192-2", MediaFormat.WEBMA, 192),
+ generateAudioStream("m4a-192-2", MediaFormat.M4A, 192),
+ generateAudioStream("webma-192-3", MediaFormat.WEBMA, 192),
+ generateAudioStream("m4a-192-3", MediaFormat.M4A, 192),
+ generateAudioStream("webma-192-4", MediaFormat.WEBMA, 192)));
// List doesn't contains this format, it should fallback to the highest bitrate audio and
// the highest quality format.
stream = testList.get(ListHelper.getHighestQualityAudioIndex(MediaFormat.MP3, testList));
@@ -250,7 +253,7 @@ public void getHighestQualityAudioFormatPreferredAbsent() {
// Adding a new format and bitrate. Adding another stream will have no impact since
// it's not a preferred format.
- testList.add(new AudioStream("", MediaFormat.WEBMA, /**/ 192));
+ testList.add(generateAudioStream("webma-192-5", MediaFormat.WEBMA, 192));
stream = testList.get(ListHelper.getHighestQualityAudioIndex(MediaFormat.MP3, testList));
assertEquals(192, stream.getAverageBitrate());
assertEquals(MediaFormat.M4A, stream.getFormat());
@@ -288,8 +291,8 @@ public void getLowestQualityAudioFormatPreferredAbsent() {
////////////////////////////////////////
List testList = new ArrayList<>(Arrays.asList(
- new AudioStream("", MediaFormat.M4A, /**/ 128),
- new AudioStream("", MediaFormat.WEBMA, /**/ 192)));
+ generateAudioStream("m4a-128", MediaFormat.M4A, 128),
+ generateAudioStream("webma-192-1", MediaFormat.WEBMA, 192)));
// List doesn't contains this format
// It should fallback to the most compact audio no matter what format it is.
AudioStream stream = testList.get(ListHelper.getMostCompactAudioIndex(
@@ -298,7 +301,7 @@ public void getLowestQualityAudioFormatPreferredAbsent() {
assertEquals(MediaFormat.M4A, stream.getFormat());
// WEBMA is more compact than M4A
- testList.add(new AudioStream("", MediaFormat.WEBMA, /**/ 128));
+ testList.add(generateAudioStream("webma-192-2", MediaFormat.WEBMA, 128));
stream = testList.get(ListHelper.getMostCompactAudioIndex(MediaFormat.MP3, testList));
assertEquals(128, stream.getAverageBitrate());
assertEquals(MediaFormat.WEBMA, stream.getFormat());
@@ -308,12 +311,12 @@ public void getLowestQualityAudioFormatPreferredAbsent() {
//////////////////////////////////////////////////////
testList = new ArrayList<>(Arrays.asList(
- new AudioStream("", MediaFormat.WEBMA, /**/ 192),
- new AudioStream("", MediaFormat.M4A, /**/ 192),
- new AudioStream("", MediaFormat.WEBMA, /**/ 256),
- new AudioStream("", MediaFormat.M4A, /**/ 192),
- new AudioStream("", MediaFormat.WEBMA, /**/ 192),
- new AudioStream("", MediaFormat.M4A, /**/ 192)));
+ generateAudioStream("webma-192-1", MediaFormat.WEBMA, 192),
+ generateAudioStream("m4a-192-1", MediaFormat.M4A, 192),
+ generateAudioStream("webma-256", MediaFormat.WEBMA, 256),
+ generateAudioStream("m4a-192-2", MediaFormat.M4A, 192),
+ generateAudioStream("webma-192-2", MediaFormat.WEBMA, 192),
+ generateAudioStream("m4a-192-3", MediaFormat.M4A, 192)));
// List doesn't contain this format
// It should fallback to the most compact audio no matter what format it is.
stream = testList.get(ListHelper.getMostCompactAudioIndex(MediaFormat.MP3, testList));
@@ -335,14 +338,14 @@ public void getLowestQualityAudioNull() {
@Test
public void getVideoDefaultStreamIndexCombinations() {
final List testList = Arrays.asList(
- new VideoStream("", MediaFormat.MPEG_4, /**/ "1080p"),
- new VideoStream("", MediaFormat.MPEG_4, /**/ "720p60"),
- new VideoStream("", MediaFormat.MPEG_4, /**/ "720p"),
- new VideoStream("", MediaFormat.WEBM, /**/ "480p"),
- new VideoStream("", MediaFormat.MPEG_4, /**/ "360p"),
- new VideoStream("", MediaFormat.WEBM, /**/ "360p"),
- new VideoStream("", MediaFormat.v3GPP, /**/ "240p60"),
- new VideoStream("", MediaFormat.WEBM, /**/ "144p"));
+ generateVideoStream("mpeg_4-1080", MediaFormat.MPEG_4, "1080p", false),
+ generateVideoStream("mpeg_4-720_60", MediaFormat.MPEG_4, "720p60", false),
+ generateVideoStream("mpeg_4-720", MediaFormat.MPEG_4, "720p", false),
+ generateVideoStream("webm-480", MediaFormat.WEBM, "480p", false),
+ generateVideoStream("mpeg_4-360", MediaFormat.MPEG_4, "360p", false),
+ generateVideoStream("webm-360", MediaFormat.WEBM, "360p", false),
+ generateVideoStream("v3gpp-240_60", MediaFormat.v3GPP, "240p60", false),
+ generateVideoStream("webm-144", MediaFormat.WEBM, "144p", false));
// exact matches
assertEquals(1, ListHelper.getVideoStreamIndex("720p60", MediaFormat.MPEG_4, testList));
@@ -375,4 +378,30 @@ public void getVideoDefaultStreamIndexCombinations() {
// Can't find a match
assertEquals(-1, ListHelper.getVideoStreamIndex("100p", null, testList));
}
+
+ @NonNull
+ private static AudioStream generateAudioStream(@NonNull final String id,
+ @Nullable final MediaFormat mediaFormat,
+ final int averageBitrate) {
+ return new AudioStream.Builder()
+ .setId(id)
+ .setContent("", true)
+ .setMediaFormat(mediaFormat)
+ .setAverageBitrate(averageBitrate)
+ .build();
+ }
+
+ @NonNull
+ private static VideoStream generateVideoStream(@NonNull final String id,
+ @Nullable final MediaFormat mediaFormat,
+ @NonNull final String resolution,
+ final boolean isVideoOnly) {
+ return new VideoStream.Builder()
+ .setId(id)
+ .setContent("", true)
+ .setIsVideoOnly(isVideoOnly)
+ .setResolution(resolution)
+ .setMediaFormat(mediaFormat)
+ .build();
+ }
}
From 7d6bf4b0cac4fced6463bfc84c40f48ef57cb00d Mon Sep 17 00:00:00 2001
From: AudricV <74829229+AudricV@users.noreply.github.com>
Date: Thu, 16 Jun 2022 11:13:31 +0200
Subject: [PATCH 067/240] Improve dialog of streams for external players and
fix use of the wrong codec in the list of available streams in it after a
codec change in Video and Audio settings
The VideoDetailFragment will now get video streams dynamically instead of storing them as a field, so the good codec can be chosen by ListHelper.
To select a stream to play, user has now to select the quality in the list of available qualities and then press the new OK button in the alert dialog.
---
.../fragments/detail/VideoDetailFragment.java | 36 +++++++++++--------
1 file changed, 22 insertions(+), 14 deletions(-)
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java
index f5bd1f363e5..bb09681f537 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java
@@ -188,7 +188,6 @@ public final class VideoDetailFragment
@Nullable
private Disposable positionSubscriber = null;
- private List videoStreamsForExternalPlayers;
private BottomSheetBehavior bottomSheetBehavior;
private BroadcastReceiver broadcastReceiver;
@@ -1615,13 +1614,6 @@ public void handleResult(@NonNull final StreamInfo info) {
binding.detailToggleSecondaryControlsView.setVisibility(View.VISIBLE);
binding.detailSecondaryControlPanel.setVisibility(View.GONE);
- final List videoStreams = removeNonUrlAndTorrentStreams(
- new ArrayList<>(currentInfo.getVideoStreams()));
- final List videoOnlyStreams = removeNonUrlAndTorrentStreams(
- new ArrayList<>(currentInfo.getVideoOnlyStreams()));
- videoStreamsForExternalPlayers = ListHelper.getSortedStreamVideosList(activity,
- videoStreams, videoOnlyStreams, false, false);
-
updateProgressInfo(info);
initThumbnailViews(info);
showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView,
@@ -2155,13 +2147,21 @@ private void showExternalPlaybackDialog() {
return;
}
+ final List videoStreams = removeNonUrlAndTorrentStreams(
+ new ArrayList<>(currentInfo.getVideoStreams()));
+ final List videoOnlyStreams = removeNonUrlAndTorrentStreams(
+ new ArrayList<>(currentInfo.getVideoOnlyStreams()));
+
final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setTitle(R.string.select_quality_external_players);
- builder.setNegativeButton(android.R.string.cancel, null);
builder.setNeutralButton(R.string.open_in_browser, (dialog, i) ->
ShareUtils.openUrlInBrowser(requireActivity(), url));
+ final List videoStreamsForExternalPlayers =
+ ListHelper.getSortedStreamVideosList(activity, videoStreams, videoOnlyStreams,
+ false, false);
if (videoStreamsForExternalPlayers.isEmpty()) {
builder.setMessage(R.string.no_video_streams_available_for_external_players);
+ builder.setPositiveButton(R.string.ok, null);
} else {
final int selectedVideoStreamIndexForExternalPlayers =
ListHelper.getDefaultResolutionIndex(activity, videoStreamsForExternalPlayers);
@@ -2173,11 +2173,19 @@ private void showExternalPlaybackDialog() {
}
builder.setSingleChoiceItems(resolutions, selectedVideoStreamIndexForExternalPlayers,
- (dialog, i) -> {
- dialog.dismiss();
- startOnExternalPlayer(activity, currentInfo,
- videoStreamsForExternalPlayers.get(i));
- });
+ null);
+ builder.setNegativeButton(R.string.cancel, null);
+ builder.setPositiveButton(R.string.ok, (dialog, i) -> {
+ final int index = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
+ // We don't have to manage the index validity because if there is no stream
+ // available for external players, this code will be not executed and if there is
+ // no stream which matches the default resolution, 0 is returned by
+ // ListHelper.getDefaultResolutionIndex.
+ // The index cannot be outside the bounds of the list as its always between 0 and
+ // the list size - 1, .
+ startOnExternalPlayer(activity, currentInfo,
+ videoStreamsForExternalPlayers.get(index));
+ });
}
builder.show();
}
From fbee310261b5ea42127febda0e64ba48df51e26a Mon Sep 17 00:00:00 2001
From: AudricV <74829229+AudricV@users.noreply.github.com>
Date: Thu, 16 Jun 2022 11:13:39 +0200
Subject: [PATCH 068/240] Move SimpleCache creation in PlayerDataSource to
avoid an IllegalStateException
This IllegalStateException, almost not reproducible, indicates that another SimpleCache instance uses the cache folder, which was so trying to be created at least twice.
Moving the SimpleCache creation in PlayerDataSource should avoid this exception.
---
.../newpipe/player/helper/CacheFactory.java | 41 ++++++++-----------
.../player/helper/PlayerDataSource.java | 34 +++++++++++++++
2 files changed, 50 insertions(+), 25 deletions(-)
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java b/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java
index 47371533ab7..b09d8d64346 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java
@@ -1,12 +1,10 @@
package org.schabi.newpipe.player.helper;
import android.content.Context;
-import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import com.google.android.exoplayer2.database.StandaloneDatabaseProvider;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSource;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
@@ -14,31 +12,26 @@
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.upstream.cache.CacheDataSink;
import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
-import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor;
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
import org.schabi.newpipe.player.datasource.YoutubeHttpDataSource;
-import java.io.File;
-
/* package-private */ final class CacheFactory implements DataSource.Factory {
- private static final String TAG = CacheFactory.class.getSimpleName();
-
- private static final String CACHE_FOLDER_NAME = "exoplayer";
private static final int CACHE_FLAGS = CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR;
- private static SimpleCache cache;
private final long maxFileSize;
private final Context context;
private final String userAgent;
private final TransferListener transferListener;
private final DataSource.Factory upstreamDataSourceFactory;
+ private final SimpleCache simpleCache;
public static class Builder {
private final Context context;
private final String userAgent;
private final TransferListener transferListener;
private DataSource.Factory upstreamDataSourceFactory;
+ private SimpleCache simpleCache;
Builder(@NonNull final Context context,
@NonNull final String userAgent,
@@ -53,8 +46,16 @@ public void setUpstreamDataSourceFactory(
this.upstreamDataSourceFactory = upstreamDataSourceFactory;
}
+ public void setSimpleCache(@NonNull final SimpleCache simpleCache) {
+ this.simpleCache = simpleCache;
+ }
+
public CacheFactory build() {
- return new CacheFactory(context, userAgent, transferListener,
+ if (simpleCache == null) {
+ throw new IllegalStateException("No SimpleCache instance has been specified. "
+ + "Please specify one with setSimpleCache");
+ }
+ return new CacheFactory(context, userAgent, transferListener, simpleCache,
upstreamDataSourceFactory);
}
}
@@ -62,25 +63,14 @@ public CacheFactory build() {
private CacheFactory(@NonNull final Context context,
@NonNull final String userAgent,
@NonNull final TransferListener transferListener,
+ @NonNull final SimpleCache simpleCache,
@Nullable final DataSource.Factory upstreamDataSourceFactory) {
this.context = context;
this.userAgent = userAgent;
this.transferListener = transferListener;
+ this.simpleCache = simpleCache;
this.upstreamDataSourceFactory = upstreamDataSourceFactory;
- final File cacheDir = new File(context.getExternalCacheDir(), CACHE_FOLDER_NAME);
- if (!cacheDir.exists()) {
- //noinspection ResultOfMethodCallIgnored
- cacheDir.mkdir();
- }
-
- if (cache == null) {
- final LeastRecentlyUsedCacheEvictor evictor
- = new LeastRecentlyUsedCacheEvictor(PlayerHelper.getPreferredCacheSize());
- cache = new SimpleCache(cacheDir, evictor, new StandaloneDatabaseProvider(context));
- Log.d(TAG, "initExoPlayerCache: cacheDir = " + cacheDir.getAbsolutePath());
- }
-
maxFileSize = PlayerHelper.getPreferredFileSize();
}
@@ -112,7 +102,8 @@ public DataSource createDataSource() {
.createDataSource();
final FileDataSource fileSource = new FileDataSource();
- final CacheDataSink dataSink = new CacheDataSink(cache, maxFileSize);
- return new CacheDataSource(cache, dataSource, fileSource, dataSink, CACHE_FLAGS, null);
+ final CacheDataSink dataSink = new CacheDataSink(simpleCache, maxFileSize);
+ return new CacheDataSource(simpleCache, dataSource, fileSource, dataSink, CACHE_FLAGS,
+ null);
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java
index 61d8baffcd8..68c9223c975 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java
@@ -1,10 +1,12 @@
package org.schabi.newpipe.player.helper;
import android.content.Context;
+import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.google.android.exoplayer2.database.StandaloneDatabaseProvider;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.source.SingleSampleMediaSource;
import com.google.android.exoplayer2.source.dash.DashMediaSource;
@@ -18,12 +20,16 @@
import com.google.android.exoplayer2.upstream.DefaultDataSource;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
import com.google.android.exoplayer2.upstream.TransferListener;
+import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor;
+import com.google.android.exoplayer2.upstream.cache.SimpleCache;
import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeOtfDashManifestCreator;
import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubePostLiveStreamDvrDashManifestCreator;
import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeProgressiveDashManifestCreator;
import org.schabi.newpipe.player.datasource.YoutubeHttpDataSource;
+import java.io.File;
+
public class PlayerDataSource {
public static final int LIVE_STREAM_EDGE_GAP_MILLIS = 10000;
@@ -43,6 +49,18 @@ public class PlayerDataSource {
*/
private static final int MAXIMUM_SIZE_CACHED_GENERATED_MANIFESTS_PER_CACHE = 500;
+ /**
+ * The folder name in which the ExoPlayer cache will be written.
+ */
+ private static final String CACHE_FOLDER_NAME = "exoplayer";
+
+ /**
+ * The {@link SimpleCache} instance which will be used to build
+ * {@link com.google.android.exoplayer2.upstream.cache.CacheDataSource}s instances (with
+ * {@link CacheFactory}).
+ */
+ private static SimpleCache cache;
+
private final int continueLoadingCheckIntervalBytes;
private final CacheFactory.Builder cacheDataSourceFactoryBuilder;
private final DataSource.Factory cachelessDataSourceFactory;
@@ -51,8 +69,24 @@ public PlayerDataSource(@NonNull final Context context,
@NonNull final String userAgent,
@NonNull final TransferListener transferListener) {
continueLoadingCheckIntervalBytes = PlayerHelper.getProgressiveLoadIntervalBytes(context);
+ final File cacheDir = new File(context.getExternalCacheDir(), CACHE_FOLDER_NAME);
+ if (!cacheDir.exists()) {
+ //noinspection ResultOfMethodCallIgnored
+ cacheDir.mkdir();
+ }
+
+ if (cache == null) {
+ final LeastRecentlyUsedCacheEvictor evictor
+ = new LeastRecentlyUsedCacheEvictor(PlayerHelper.getPreferredCacheSize());
+ cache = new SimpleCache(cacheDir, evictor, new StandaloneDatabaseProvider(context));
+ Log.d(PlayerDataSource.class.getSimpleName(), "initExoPlayerCache: cacheDir = "
+ + cacheDir.getAbsolutePath());
+ }
+
cacheDataSourceFactoryBuilder = new CacheFactory.Builder(context, userAgent,
transferListener);
+ cacheDataSourceFactoryBuilder.setSimpleCache(cache);
+
cachelessDataSourceFactory = new DefaultDataSource.Factory(context,
new DefaultHttpDataSource.Factory().setUserAgent(userAgent))
.setTransferListener(transferListener);
From ef20d9b91a7cca87d2dc1f4860d1ee3b80f98178 Mon Sep 17 00:00:00 2001
From: Stypox
Date: Sun, 8 May 2022 14:39:24 +0200
Subject: [PATCH 069/240] Move stream's cache key generation in
PlaybackResolver and improve PlaybackResolver's code
---
.../newpipe/player/helper/PlayerHelper.java | 50 ------------
.../resolver/AudioPlaybackResolver.java | 3 +-
.../player/resolver/PlaybackResolver.java | 76 +++++++++++++++++++
.../resolver/VideoPlaybackResolver.java | 4 +-
4 files changed, 79 insertions(+), 54 deletions(-)
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java
index d924f931476..2131861bff6 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java
@@ -3,8 +3,6 @@
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF;
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
-import static org.schabi.newpipe.extractor.stream.AudioStream.UNKNOWN_BITRATE;
-import static org.schabi.newpipe.extractor.stream.VideoStream.RESOLUTION_UNKNOWN;
import static org.schabi.newpipe.player.Player.IDLE_WINDOW_FLAGS;
import static org.schabi.newpipe.player.Player.PLAYER_TYPE;
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_ALWAYS;
@@ -47,11 +45,9 @@
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.MediaFormat;
-import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
-import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.extractor.utils.Utils;
import org.schabi.newpipe.player.MainPlayer;
import org.schabi.newpipe.player.Player;
@@ -197,52 +193,6 @@ public static String resizeTypeOf(@NonNull final Context context,
}
}
- @NonNull
- public static String cacheKeyOf(@NonNull final StreamInfo info,
- @NonNull final VideoStream videoStream) {
- String cacheKey = info.getUrl() + " " + videoStream.getId();
-
- final String resolution = videoStream.getResolution();
- final MediaFormat mediaFormat = videoStream.getFormat();
- if (resolution.equals(RESOLUTION_UNKNOWN) && mediaFormat == null) {
- // The hash code is only used in the cache key in the case when the resolution and the
- // media format are unknown
- cacheKey += " " + videoStream.hashCode();
- } else {
- if (mediaFormat != null) {
- cacheKey += " " + videoStream.getFormat().getName();
- }
- if (!resolution.equals(RESOLUTION_UNKNOWN)) {
- cacheKey += " " + resolution;
- }
- }
-
- return cacheKey;
- }
-
- @NonNull
- public static String cacheKeyOf(@NonNull final StreamInfo info,
- @NonNull final AudioStream audioStream) {
- String cacheKey = info.getUrl() + " " + audioStream.getId();
-
- final int averageBitrate = audioStream.getAverageBitrate();
- final MediaFormat mediaFormat = audioStream.getFormat();
- if (averageBitrate == UNKNOWN_BITRATE && mediaFormat == null) {
- // The hash code is only used in the cache key in the case when the resolution and the
- // media format are unknown
- cacheKey += " " + audioStream.hashCode();
- } else {
- if (mediaFormat != null) {
- cacheKey += " " + audioStream.getFormat().getName();
- }
- if (averageBitrate != UNKNOWN_BITRATE) {
- cacheKey += " " + averageBitrate;
- }
- }
-
- return cacheKey;
- }
-
/**
* Given a {@link StreamInfo} and the existing queue items,
* provide the {@link SinglePlayQueue} consisting of the next video for auto queueing.
diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java
index 765475b2faa..85c15faf154 100644
--- a/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java
+++ b/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java
@@ -13,7 +13,6 @@
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.player.helper.PlayerDataSource;
-import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.player.mediaitem.MediaItemTag;
import org.schabi.newpipe.player.mediaitem.StreamInfoTag;
import org.schabi.newpipe.util.ListHelper;
@@ -57,7 +56,7 @@ public MediaSource resolve(@NonNull final StreamInfo info) {
try {
return PlaybackResolver.buildMediaSource(
- dataSource, audio, info, PlayerHelper.cacheKeyOf(info, audio), tag);
+ dataSource, audio, info, PlaybackResolver.cacheKeyOf(info, audio), tag);
} catch (final IOException e) {
Log.e(TAG, "Unable to create audio source:", e);
return null;
diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java
index 4c1b67dfc6f..3cbca7628c5 100644
--- a/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java
+++ b/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java
@@ -1,5 +1,7 @@
package org.schabi.newpipe.player.resolver;
+import static org.schabi.newpipe.extractor.stream.AudioStream.UNKNOWN_BITRATE;
+import static org.schabi.newpipe.extractor.stream.VideoStream.RESOLUTION_UNKNOWN;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
import static org.schabi.newpipe.player.helper.PlayerDataSource.LIVE_STREAM_EDGE_GAP_MILLIS;
@@ -20,6 +22,7 @@
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest;
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser;
+import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.extractor.services.youtube.ItagItem;
import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.CreationException;
@@ -49,6 +52,79 @@
public interface PlaybackResolver extends Resolver {
String TAG = PlaybackResolver.class.getSimpleName();
+ @NonNull
+ private static StringBuilder commonCacheKeyOf(@NonNull final StreamInfo info,
+ @NonNull final Stream stream,
+ final boolean resolutionOrBitrateUnknown) {
+ // stream info service id
+ final StringBuilder cacheKey = new StringBuilder(info.getServiceId());
+
+ // stream info id
+ cacheKey.append(" ");
+ cacheKey.append(info.getId());
+
+ // stream id (even if unknown)
+ cacheKey.append(" ");
+ cacheKey.append(stream.getId());
+
+ // mediaFormat (if not null)
+ final MediaFormat mediaFormat = stream.getFormat();
+ if (mediaFormat != null) {
+ cacheKey.append(" ");
+ cacheKey.append(mediaFormat.getName());
+ }
+
+ // content (only if other information is missing)
+ // If the media format and the resolution/bitrate are both missing, then we don't have
+ // enough information to distinguish this stream from other streams.
+ // So, only in that case, we use the content (i.e. url or manifest) to differentiate
+ // between streams.
+ // Note that if the content were used even when other information is present, then two
+ // streams with the same stats but with different contents (e.g. because the url was
+ // refreshed) will be considered different (i.e. with a different cacheKey), making the
+ // cache useless.
+ if (resolutionOrBitrateUnknown && mediaFormat == null) {
+ cacheKey.append(" ");
+ Objects.hash(stream.getContent(), stream.getManifestUrl());
+ }
+
+ return cacheKey;
+ }
+
+ @NonNull
+ static String cacheKeyOf(@NonNull final StreamInfo info,
+ @NonNull final VideoStream videoStream) {
+ final boolean resolutionUnknown = videoStream.getResolution().equals(RESOLUTION_UNKNOWN);
+ final StringBuilder cacheKey = commonCacheKeyOf(info, videoStream, resolutionUnknown);
+
+ // resolution (if known)
+ if (!resolutionUnknown) {
+ cacheKey.append(" ");
+ cacheKey.append(videoStream.getResolution());
+ }
+
+ // isVideoOnly
+ cacheKey.append(" ");
+ cacheKey.append(videoStream.isVideoOnly());
+
+ return cacheKey.toString();
+ }
+
+ @NonNull
+ static String cacheKeyOf(@NonNull final StreamInfo info,
+ @NonNull final AudioStream audioStream) {
+ final boolean averageBitrateUnknown = audioStream.getAverageBitrate() == UNKNOWN_BITRATE;
+ final StringBuilder cacheKey = commonCacheKeyOf(info, audioStream, averageBitrateUnknown);
+
+ // averageBitrate (if known)
+ if (!averageBitrateUnknown) {
+ cacheKey.append(" ");
+ cacheKey.append(audioStream.getAverageBitrate());
+ }
+
+ return cacheKey.toString();
+ }
+
@Nullable
static MediaSource maybeBuildLiveMediaSource(@NonNull final PlayerDataSource dataSource,
@NonNull final StreamInfo info) {
diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java
index 24ca2e63a0e..317c49fc93a 100644
--- a/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java
+++ b/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java
@@ -95,7 +95,7 @@ public MediaSource resolve(@NonNull final StreamInfo info) {
if (video != null) {
try {
final MediaSource streamSource = PlaybackResolver.buildMediaSource(
- dataSource, video, info, PlayerHelper.cacheKeyOf(info, video), tag);
+ dataSource, video, info, PlaybackResolver.cacheKeyOf(info, video), tag);
mediaSources.add(streamSource);
} catch (final IOException e) {
Log.e(TAG, "Unable to create video source:", e);
@@ -114,7 +114,7 @@ public MediaSource resolve(@NonNull final StreamInfo info) {
if (audio != null && (video == null || video.isVideoOnly())) {
try {
final MediaSource audioSource = PlaybackResolver.buildMediaSource(
- dataSource, audio, info, PlayerHelper.cacheKeyOf(info, audio), tag);
+ dataSource, audio, info, PlaybackResolver.cacheKeyOf(info, audio), tag);
mediaSources.add(audioSource);
streamSourceType = SourceType.VIDEO_WITH_SEPARATED_AUDIO;
} catch (final IOException e) {
From 7ce2250d8523dd482f8d8df184ac38356fa48c17 Mon Sep 17 00:00:00 2001
From: Stypox
Date: Sat, 21 May 2022 11:12:37 +0200
Subject: [PATCH 070/240] Improve CacheFactory and PlayerDataSource code
---
.../newpipe/player/helper/CacheFactory.java | 85 ++---------
.../player/helper/PlayerDataSource.java | 142 ++++++++++--------
2 files changed, 94 insertions(+), 133 deletions(-)
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java b/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java
index b09d8d64346..d189616d193 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java
@@ -3,107 +3,44 @@
import android.content.Context;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSource;
-import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
import com.google.android.exoplayer2.upstream.FileDataSource;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.upstream.cache.CacheDataSink;
import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
-import org.schabi.newpipe.player.datasource.YoutubeHttpDataSource;
-
-/* package-private */ final class CacheFactory implements DataSource.Factory {
+final class CacheFactory implements DataSource.Factory {
private static final int CACHE_FLAGS = CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR;
- private final long maxFileSize;
private final Context context;
- private final String userAgent;
private final TransferListener transferListener;
private final DataSource.Factory upstreamDataSourceFactory;
- private final SimpleCache simpleCache;
-
- public static class Builder {
- private final Context context;
- private final String userAgent;
- private final TransferListener transferListener;
- private DataSource.Factory upstreamDataSourceFactory;
- private SimpleCache simpleCache;
-
- Builder(@NonNull final Context context,
- @NonNull final String userAgent,
- @NonNull final TransferListener transferListener) {
- this.context = context;
- this.userAgent = userAgent;
- this.transferListener = transferListener;
- }
-
- public void setUpstreamDataSourceFactory(
- @Nullable final DataSource.Factory upstreamDataSourceFactory) {
- this.upstreamDataSourceFactory = upstreamDataSourceFactory;
- }
-
- public void setSimpleCache(@NonNull final SimpleCache simpleCache) {
- this.simpleCache = simpleCache;
- }
+ private final SimpleCache cache;
- public CacheFactory build() {
- if (simpleCache == null) {
- throw new IllegalStateException("No SimpleCache instance has been specified. "
- + "Please specify one with setSimpleCache");
- }
- return new CacheFactory(context, userAgent, transferListener, simpleCache,
- upstreamDataSourceFactory);
- }
- }
-
- private CacheFactory(@NonNull final Context context,
- @NonNull final String userAgent,
- @NonNull final TransferListener transferListener,
- @NonNull final SimpleCache simpleCache,
- @Nullable final DataSource.Factory upstreamDataSourceFactory) {
+ CacheFactory(final Context context,
+ final TransferListener transferListener,
+ final SimpleCache cache,
+ final DataSource.Factory upstreamDataSourceFactory) {
this.context = context;
- this.userAgent = userAgent;
this.transferListener = transferListener;
- this.simpleCache = simpleCache;
+ this.cache = cache;
this.upstreamDataSourceFactory = upstreamDataSourceFactory;
-
- maxFileSize = PlayerHelper.getPreferredFileSize();
}
@NonNull
@Override
public DataSource createDataSource() {
-
- final DataSource.Factory upstreamDataSourceFactoryToUse;
- if (upstreamDataSourceFactory == null) {
- upstreamDataSourceFactoryToUse = new DefaultHttpDataSource.Factory()
- .setUserAgent(userAgent);
- } else {
- if (upstreamDataSourceFactory instanceof DefaultHttpDataSource.Factory) {
- upstreamDataSourceFactoryToUse =
- ((DefaultHttpDataSource.Factory) upstreamDataSourceFactory)
- .setUserAgent(userAgent);
- } else if (upstreamDataSourceFactory instanceof YoutubeHttpDataSource.Factory) {
- upstreamDataSourceFactoryToUse =
- ((YoutubeHttpDataSource.Factory) upstreamDataSourceFactory)
- .setUserAgentForNonMobileStreams(userAgent);
- } else {
- upstreamDataSourceFactoryToUse = upstreamDataSourceFactory;
- }
- }
-
final DefaultDataSource dataSource = new DefaultDataSource.Factory(context,
- upstreamDataSourceFactoryToUse)
+ upstreamDataSourceFactory)
.setTransferListener(transferListener)
.createDataSource();
final FileDataSource fileSource = new FileDataSource();
- final CacheDataSink dataSink = new CacheDataSink(simpleCache, maxFileSize);
- return new CacheDataSource(simpleCache, dataSource, fileSource, dataSink, CACHE_FLAGS,
- null);
+ final CacheDataSink dataSink
+ = new CacheDataSink(cache, PlayerHelper.getPreferredFileSize());
+ return new CacheDataSource(cache, dataSource, fileSource, dataSink, CACHE_FLAGS, null);
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java
index 68c9223c975..f732e834f75 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java
@@ -3,7 +3,6 @@
import android.content.Context;
import android.util.Log;
-import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.database.StandaloneDatabaseProvider;
@@ -31,6 +30,7 @@
import java.io.File;
public class PlayerDataSource {
+ public static final String TAG = PlayerDataSource.class.getSimpleName();
public static final int LIVE_STREAM_EDGE_GAP_MILLIS = 10000;
@@ -47,7 +47,7 @@ public class PlayerDataSource {
* {@link YoutubeProgressiveDashManifestCreator}, {@link YoutubeOtfDashManifestCreator} and
* {@link YoutubePostLiveStreamDvrDashManifestCreator}.
*/
- private static final int MAXIMUM_SIZE_CACHED_GENERATED_MANIFESTS_PER_CACHE = 500;
+ private static final int MAX_MANIFEST_CACHE_SIZE = 500;
/**
* The folder name in which the ExoPlayer cache will be written.
@@ -61,44 +61,53 @@ public class PlayerDataSource {
*/
private static SimpleCache cache;
- private final int continueLoadingCheckIntervalBytes;
- private final CacheFactory.Builder cacheDataSourceFactoryBuilder;
+
+ private final int progressiveLoadIntervalBytes;
+
+ // Generic Data Source Factories (without or with cache)
private final DataSource.Factory cachelessDataSourceFactory;
+ private final CacheFactory cacheDataSourceFactory;
- public PlayerDataSource(@NonNull final Context context,
- @NonNull final String userAgent,
- @NonNull final TransferListener transferListener) {
- continueLoadingCheckIntervalBytes = PlayerHelper.getProgressiveLoadIntervalBytes(context);
- final File cacheDir = new File(context.getExternalCacheDir(), CACHE_FOLDER_NAME);
- if (!cacheDir.exists()) {
- //noinspection ResultOfMethodCallIgnored
- cacheDir.mkdir();
- }
+ // YouTube-specific Data Source Factories (with cache)
+ // They use YoutubeHttpDataSource.Factory, with different parameters each
+ private final CacheFactory ytHlsCacheDataSourceFactory;
+ private final CacheFactory ytDashCacheDataSourceFactory;
+ private final CacheFactory ytProgressiveDashCacheDataSourceFactory;
- if (cache == null) {
- final LeastRecentlyUsedCacheEvictor evictor
- = new LeastRecentlyUsedCacheEvictor(PlayerHelper.getPreferredCacheSize());
- cache = new SimpleCache(cacheDir, evictor, new StandaloneDatabaseProvider(context));
- Log.d(PlayerDataSource.class.getSimpleName(), "initExoPlayerCache: cacheDir = "
- + cacheDir.getAbsolutePath());
- }
- cacheDataSourceFactoryBuilder = new CacheFactory.Builder(context, userAgent,
- transferListener);
- cacheDataSourceFactoryBuilder.setSimpleCache(cache);
+ public PlayerDataSource(final Context context,
+ final String userAgent,
+ final TransferListener transferListener) {
+ progressiveLoadIntervalBytes = PlayerHelper.getProgressiveLoadIntervalBytes(context);
+
+ // make sure the static cache was created: needed by CacheFactories below
+ instantiateCacheIfNeeded(context);
+
+ // generic data source factories use DefaultHttpDataSource.Factory
cachelessDataSourceFactory = new DefaultDataSource.Factory(context,
new DefaultHttpDataSource.Factory().setUserAgent(userAgent))
.setTransferListener(transferListener);
-
- YoutubeProgressiveDashManifestCreator.getCache().setMaximumSize(
- MAXIMUM_SIZE_CACHED_GENERATED_MANIFESTS_PER_CACHE);
- YoutubeOtfDashManifestCreator.getCache().setMaximumSize(
- MAXIMUM_SIZE_CACHED_GENERATED_MANIFESTS_PER_CACHE);
+ cacheDataSourceFactory = new CacheFactory(context, transferListener, cache,
+ new DefaultHttpDataSource.Factory().setUserAgent(userAgent));
+
+ // YouTube-specific data source factories use getYoutubeHttpDataSourceFactory()
+ ytHlsCacheDataSourceFactory = new CacheFactory(context, transferListener, cache,
+ getYoutubeHttpDataSourceFactory(false, false, userAgent));
+ ytDashCacheDataSourceFactory = new CacheFactory(context, transferListener, cache,
+ getYoutubeHttpDataSourceFactory(true, true, userAgent));
+ ytProgressiveDashCacheDataSourceFactory = new CacheFactory(context, transferListener, cache,
+ getYoutubeHttpDataSourceFactory(false, true, userAgent));
+
+ // set the maximum size to manifest creators
+ YoutubeProgressiveDashManifestCreator.getCache().setMaximumSize(MAX_MANIFEST_CACHE_SIZE);
+ YoutubeOtfDashManifestCreator.getCache().setMaximumSize(MAX_MANIFEST_CACHE_SIZE);
YoutubePostLiveStreamDvrDashManifestCreator.getCache().setMaximumSize(
- MAXIMUM_SIZE_CACHED_GENERATED_MANIFESTS_PER_CACHE);
+ MAX_MANIFEST_CACHE_SIZE);
}
+
+ //region Live media source factories
public SsMediaSource.Factory getLiveSsMediaSourceFactory() {
return getSSMediaSourceFactory().setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS);
}
@@ -118,26 +127,26 @@ public DashMediaSource.Factory getLiveDashMediaSourceFactory() {
getDefaultDashChunkSourceFactory(cachelessDataSourceFactory),
cachelessDataSourceFactory);
}
+ //endregion
+
+ //region Generic media source factories
public HlsMediaSource.Factory getHlsMediaSourceFactory(
@Nullable final HlsPlaylistParserFactory hlsPlaylistParserFactory) {
- final HlsMediaSource.Factory factory = new HlsMediaSource.Factory(
- cacheDataSourceFactoryBuilder.build());
- if (hlsPlaylistParserFactory != null) {
- factory.setPlaylistParserFactory(hlsPlaylistParserFactory);
- }
+ final HlsMediaSource.Factory factory = new HlsMediaSource.Factory(cacheDataSourceFactory);
+ factory.setPlaylistParserFactory(hlsPlaylistParserFactory);
return factory;
}
public DashMediaSource.Factory getDashMediaSourceFactory() {
return new DashMediaSource.Factory(
- getDefaultDashChunkSourceFactory(cacheDataSourceFactoryBuilder.build()),
- cacheDataSourceFactoryBuilder.build());
+ getDefaultDashChunkSourceFactory(cacheDataSourceFactory),
+ cacheDataSourceFactory);
}
public ProgressiveMediaSource.Factory getProgressiveMediaSourceFactory() {
- return new ProgressiveMediaSource.Factory(cacheDataSourceFactoryBuilder.build())
- .setContinueLoadingCheckIntervalBytes(continueLoadingCheckIntervalBytes);
+ return new ProgressiveMediaSource.Factory(cacheDataSourceFactory)
+ .setContinueLoadingCheckIntervalBytes(progressiveLoadIntervalBytes);
}
public SsMediaSource.Factory getSSMediaSourceFactory() {
@@ -147,42 +156,57 @@ public SsMediaSource.Factory getSSMediaSourceFactory() {
}
public SingleSampleMediaSource.Factory getSingleSampleMediaSourceFactory() {
- return new SingleSampleMediaSource.Factory(cacheDataSourceFactoryBuilder.build());
+ return new SingleSampleMediaSource.Factory(cacheDataSourceFactory);
}
+ //endregion
- public DashMediaSource.Factory getYoutubeDashMediaSourceFactory() {
- cacheDataSourceFactoryBuilder.setUpstreamDataSourceFactory(
- getYoutubeHttpDataSourceFactory(true, true));
- return new DashMediaSource.Factory(
- getDefaultDashChunkSourceFactory(cacheDataSourceFactoryBuilder.build()),
- cacheDataSourceFactoryBuilder.build());
- }
+ //region YouTube media source factories
public HlsMediaSource.Factory getYoutubeHlsMediaSourceFactory() {
- cacheDataSourceFactoryBuilder.setUpstreamDataSourceFactory(
- getYoutubeHttpDataSourceFactory(false, false));
- return new HlsMediaSource.Factory(cacheDataSourceFactoryBuilder.build());
+ return new HlsMediaSource.Factory(ytHlsCacheDataSourceFactory);
+ }
+
+ public DashMediaSource.Factory getYoutubeDashMediaSourceFactory() {
+ return new DashMediaSource.Factory(
+ getDefaultDashChunkSourceFactory(ytDashCacheDataSourceFactory),
+ ytDashCacheDataSourceFactory);
}
public ProgressiveMediaSource.Factory getYoutubeProgressiveMediaSourceFactory() {
- cacheDataSourceFactoryBuilder.setUpstreamDataSourceFactory(
- getYoutubeHttpDataSourceFactory(false, true));
- return new ProgressiveMediaSource.Factory(cacheDataSourceFactoryBuilder.build())
- .setContinueLoadingCheckIntervalBytes(continueLoadingCheckIntervalBytes);
+ return new ProgressiveMediaSource.Factory(ytProgressiveDashCacheDataSourceFactory)
+ .setContinueLoadingCheckIntervalBytes(progressiveLoadIntervalBytes);
}
+ //endregion
- @NonNull
- private DefaultDashChunkSource.Factory getDefaultDashChunkSourceFactory(
+
+ //region Static methods
+ private static DefaultDashChunkSource.Factory getDefaultDashChunkSourceFactory(
final DataSource.Factory dataSourceFactory) {
return new DefaultDashChunkSource.Factory(dataSourceFactory);
}
- @NonNull
- private YoutubeHttpDataSource.Factory getYoutubeHttpDataSourceFactory(
+ private static YoutubeHttpDataSource.Factory getYoutubeHttpDataSourceFactory(
final boolean rangeParameterEnabled,
- final boolean rnParameterEnabled) {
+ final boolean rnParameterEnabled,
+ final String userAgent) {
return new YoutubeHttpDataSource.Factory()
.setRangeParameterEnabled(rangeParameterEnabled)
- .setRnParameterEnabled(rnParameterEnabled);
+ .setRnParameterEnabled(rnParameterEnabled)
+ .setUserAgentForNonMobileStreams(userAgent);
+ }
+
+ private static void instantiateCacheIfNeeded(final Context context) {
+ if (cache == null) {
+ final File cacheDir = new File(context.getExternalCacheDir(), CACHE_FOLDER_NAME);
+ Log.d(TAG, "instantiateCacheIfNeeded: cacheDir = " + cacheDir.getAbsolutePath());
+ if (!cacheDir.exists() && !cacheDir.mkdir()) {
+ Log.w(TAG, "instantiateCacheIfNeeded: could not create cache dir");
+ }
+
+ final LeastRecentlyUsedCacheEvictor evictor
+ = new LeastRecentlyUsedCacheEvictor(PlayerHelper.getPreferredCacheSize());
+ cache = new SimpleCache(cacheDir, evictor, new StandaloneDatabaseProvider(context));
+ }
}
+ //endregion
}
From fa46b7bf85caa29b94d9194e41a2ae2ace4db2c8 Mon Sep 17 00:00:00 2001
From: Stypox
Date: Sat, 21 May 2022 11:27:14 +0200
Subject: [PATCH 071/240] Add comments and use downloader user agent in YT data
source
YoutubeHttpDataSource
---
.../datasource/YoutubeHttpDataSource.java | 51 +++++++------------
.../player/helper/PlayerDataSource.java | 12 ++---
2 files changed, 22 insertions(+), 41 deletions(-)
diff --git a/app/src/main/java/org/schabi/newpipe/player/datasource/YoutubeHttpDataSource.java b/app/src/main/java/org/schabi/newpipe/player/datasource/YoutubeHttpDataSource.java
index acf9c6a4760..c9abe65f62c 100644
--- a/app/src/main/java/org/schabi/newpipe/player/datasource/YoutubeHttpDataSource.java
+++ b/app/src/main/java/org/schabi/newpipe/player/datasource/YoutubeHttpDataSource.java
@@ -44,6 +44,8 @@
import com.google.common.collect.Sets;
import com.google.common.net.HttpHeaders;
+import org.schabi.newpipe.DownloaderImpl;
+
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
@@ -69,6 +71,10 @@
* (only where it's relevant) and also more parameters, such as {@code rn} and replaces the use of
* the {@code Range} header by the corresponding parameter ({@code range}), if enabled.
*
+ *
+ * There are many unused methods in this class because everything was copied from {@link
+ * com.google.android.exoplayer2.upstream.DefaultHttpDataSource} with as little changes as possible.
+ * SonarQube warnings were also suppressed for the same reason.
*/
@SuppressWarnings({"squid:S3011", "squid:S4738"})
public final class YoutubeHttpDataSource extends BaseDataSource implements HttpDataSource {
@@ -89,8 +95,6 @@ public static final class Factory implements HttpDataSource.Factory {
private boolean allowCrossProtocolRedirects;
private boolean keepPostFor302Redirects;
- @Nullable
- private String userAgentForNonMobileStreams;
private boolean rangeParameterEnabled;
private boolean rnParameterEnabled;
@@ -111,25 +115,6 @@ public Factory setDefaultRequestProperties(
return this;
}
- /**
- * Sets the user agent that will be used, only for non-mobile streams.
- *
- *
- * The default is {@code null}, which causes the default user agent of the underlying
- * platform to be used.
- *
- *
- * @param userAgentForNonMobileStreamsValue The user agent that will be used for non-mobile
- * streams, or {@code null} to use the default
- * user agent of the underlying platform.
- * @return This factory.
- */
- public Factory setUserAgentForNonMobileStreams(
- @Nullable final String userAgentForNonMobileStreamsValue) {
- userAgentForNonMobileStreams = userAgentForNonMobileStreamsValue;
- return this;
- }
-
/**
* Sets the connect timeout, in milliseconds.
*
@@ -262,7 +247,6 @@ public Factory setKeepPostFor302Redirects(final boolean keepPostFor302RedirectsV
@Override
public YoutubeHttpDataSource createDataSource() {
final YoutubeHttpDataSource dataSource = new YoutubeHttpDataSource(
- userAgentForNonMobileStreams,
connectTimeoutMs,
readTimeoutMs,
allowCrossProtocolRedirects,
@@ -294,8 +278,6 @@ public YoutubeHttpDataSource createDataSource() {
private final int connectTimeoutMillis;
private final int readTimeoutMillis;
@Nullable
- private final String userAgent;
- @Nullable
private final RequestProperties defaultRequestProperties;
private final RequestProperties requestProperties;
private final boolean keepPostFor302Redirects;
@@ -316,8 +298,7 @@ public YoutubeHttpDataSource createDataSource() {
private long requestNumber;
@SuppressWarnings("checkstyle:ParameterNumber")
- private YoutubeHttpDataSource(@Nullable final String userAgent,
- final int connectTimeoutMillis,
+ private YoutubeHttpDataSource(final int connectTimeoutMillis,
final int readTimeoutMillis,
final boolean allowCrossProtocolRedirects,
final boolean rangeParameterEnabled,
@@ -326,7 +307,6 @@ private YoutubeHttpDataSource(@Nullable final String userAgent,
@Nullable final Predicate contentTypePredicate,
final boolean keepPostFor302Redirects) {
super(true);
- this.userAgent = userAgent;
this.connectTimeoutMillis = connectTimeoutMillis;
this.readTimeoutMillis = readTimeoutMillis;
this.allowCrossProtocolRedirects = allowCrossProtocolRedirects;
@@ -637,6 +617,8 @@ private HttpURLConnection makeConnection(
final boolean allowGzip,
final boolean followRedirects,
final Map requestParameters) throws IOException {
+ // This is the method that contains breaking changes with respect to DefaultHttpDataSource!
+
String requestUrl = url.toString();
// Don't add the request number parameter if it has been already added (for instance in
@@ -687,18 +669,19 @@ private HttpURLConnection makeConnection(
httpURLConnection.setRequestProperty(HttpHeaders.TE, "trailers");
- final boolean isAnAndroidStreamingUrl = isAndroidStreamingUrl(requestUrl);
- final boolean isAnIosStreamingUrl = isIosStreamingUrl(requestUrl);
- if (isAnAndroidStreamingUrl) {
+ final boolean isAndroidStreamingUrl = isAndroidStreamingUrl(requestUrl);
+ final boolean isIosStreamingUrl = isIosStreamingUrl(requestUrl);
+ if (isAndroidStreamingUrl) {
// Improvement which may be done: find the content country used to request YouTube
// contents to add it in the user agent instead of using the default
httpURLConnection.setRequestProperty(HttpHeaders.USER_AGENT,
getAndroidUserAgent(null));
- } else if (isAnIosStreamingUrl) {
+ } else if (isIosStreamingUrl) {
httpURLConnection.setRequestProperty(HttpHeaders.USER_AGENT,
getIosUserAgent(null));
- } else if (userAgent != null) {
- httpURLConnection.setRequestProperty(HttpHeaders.USER_AGENT, userAgent);
+ } else {
+ // non-mobile user agent
+ httpURLConnection.setRequestProperty(HttpHeaders.USER_AGENT, DownloaderImpl.USER_AGENT);
}
httpURLConnection.setRequestProperty(HttpHeaders.ACCEPT_ENCODING,
@@ -707,7 +690,7 @@ private HttpURLConnection makeConnection(
httpURLConnection.setDoOutput(httpBody != null);
// Mobile clients uses POST requests to fetch contents
- httpURLConnection.setRequestMethod(isAnAndroidStreamingUrl || isAnIosStreamingUrl
+ httpURLConnection.setRequestMethod(isAndroidStreamingUrl || isIosStreamingUrl
? "POST"
: DataSpec.getStringForHttpMethod(httpMethod));
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java
index f732e834f75..8b7689bac2e 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java
@@ -93,11 +93,11 @@ public PlayerDataSource(final Context context,
// YouTube-specific data source factories use getYoutubeHttpDataSourceFactory()
ytHlsCacheDataSourceFactory = new CacheFactory(context, transferListener, cache,
- getYoutubeHttpDataSourceFactory(false, false, userAgent));
+ getYoutubeHttpDataSourceFactory(false, false));
ytDashCacheDataSourceFactory = new CacheFactory(context, transferListener, cache,
- getYoutubeHttpDataSourceFactory(true, true, userAgent));
+ getYoutubeHttpDataSourceFactory(true, true));
ytProgressiveDashCacheDataSourceFactory = new CacheFactory(context, transferListener, cache,
- getYoutubeHttpDataSourceFactory(false, true, userAgent));
+ getYoutubeHttpDataSourceFactory(false, true));
// set the maximum size to manifest creators
YoutubeProgressiveDashManifestCreator.getCache().setMaximumSize(MAX_MANIFEST_CACHE_SIZE);
@@ -187,12 +187,10 @@ private static DefaultDashChunkSource.Factory getDefaultDashChunkSourceFactory(
private static YoutubeHttpDataSource.Factory getYoutubeHttpDataSourceFactory(
final boolean rangeParameterEnabled,
- final boolean rnParameterEnabled,
- final String userAgent) {
+ final boolean rnParameterEnabled) {
return new YoutubeHttpDataSource.Factory()
.setRangeParameterEnabled(rangeParameterEnabled)
- .setRnParameterEnabled(rnParameterEnabled)
- .setUserAgentForNonMobileStreams(userAgent);
+ .setRnParameterEnabled(rnParameterEnabled);
}
private static void instantiateCacheIfNeeded(final Context context) {
From 8445c381c5eba5d8b10077d0d0eea3e422b48964 Mon Sep 17 00:00:00 2001
From: Stypox
Date: Sat, 21 May 2022 11:29:19 +0200
Subject: [PATCH 072/240] Use DownloaderImpl.USER_AGENT directly
instead of passing it as a parameter
---
app/src/main/java/org/schabi/newpipe/player/Player.java | 3 +--
.../org/schabi/newpipe/player/helper/PlayerDataSource.java | 6 +++---
2 files changed, 4 insertions(+), 5 deletions(-)
diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java
index d2aed76238c..316b72a0925 100644
--- a/app/src/main/java/org/schabi/newpipe/player/Player.java
+++ b/app/src/main/java/org/schabi/newpipe/player/Player.java
@@ -150,7 +150,6 @@
import com.squareup.picasso.Picasso;
import com.squareup.picasso.Target;
-import org.schabi.newpipe.DownloaderImpl;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.stream.model.StreamEntity;
@@ -429,7 +428,7 @@ public Player(@NonNull final MainPlayer service) {
setupBroadcastReceiver();
trackSelector = new DefaultTrackSelector(context, PlayerHelper.getQualitySelector());
- final PlayerDataSource dataSource = new PlayerDataSource(context, DownloaderImpl.USER_AGENT,
+ final PlayerDataSource dataSource = new PlayerDataSource(context,
new DefaultBandwidthMeter.Builder(context).build());
loadController = new LoadController();
renderFactory = new DefaultRenderersFactory(context);
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java
index 8b7689bac2e..8cb423b5101 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java
@@ -22,6 +22,7 @@
import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor;
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
+import org.schabi.newpipe.DownloaderImpl;
import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeOtfDashManifestCreator;
import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubePostLiveStreamDvrDashManifestCreator;
import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeProgressiveDashManifestCreator;
@@ -76,7 +77,6 @@ public class PlayerDataSource {
public PlayerDataSource(final Context context,
- final String userAgent,
final TransferListener transferListener) {
progressiveLoadIntervalBytes = PlayerHelper.getProgressiveLoadIntervalBytes(context);
@@ -86,10 +86,10 @@ public PlayerDataSource(final Context context,
// generic data source factories use DefaultHttpDataSource.Factory
cachelessDataSourceFactory = new DefaultDataSource.Factory(context,
- new DefaultHttpDataSource.Factory().setUserAgent(userAgent))
+ new DefaultHttpDataSource.Factory().setUserAgent(DownloaderImpl.USER_AGENT))
.setTransferListener(transferListener);
cacheDataSourceFactory = new CacheFactory(context, transferListener, cache,
- new DefaultHttpDataSource.Factory().setUserAgent(userAgent));
+ new DefaultHttpDataSource.Factory().setUserAgent(DownloaderImpl.USER_AGENT));
// YouTube-specific data source factories use getYoutubeHttpDataSourceFactory()
ytHlsCacheDataSourceFactory = new CacheFactory(context, transferListener, cache,
From e5ffa2aa096697bac6e210fb7657ac3e79f691a0 Mon Sep 17 00:00:00 2001
From: Stypox
Date: Sat, 21 May 2022 12:00:02 +0200
Subject: [PATCH 073/240] Add comments to PlaybackResolver and remove useless
@NonNull
---
.../player/resolver/PlaybackResolver.java | 183 ++++++++++--------
1 file changed, 105 insertions(+), 78 deletions(-)
diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java
index 3cbca7628c5..3d11f0e448b 100644
--- a/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java
+++ b/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java
@@ -8,6 +8,8 @@
import android.net.Uri;
import android.util.Log;
+import androidx.annotation.Nullable;
+
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.source.MediaSource;
@@ -41,20 +43,23 @@
import org.schabi.newpipe.player.mediaitem.StreamInfoTag;
import org.schabi.newpipe.util.StreamTypeUtil;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
+/**
+ * This interface is just a shorthand for {@link Resolver} with {@link StreamInfo} as source and
+ * {@link MediaSource} as product. It contains many static methods that can be used by classes
+ * implementing this interface, and nothing else.
+ */
public interface PlaybackResolver extends Resolver {
String TAG = PlaybackResolver.class.getSimpleName();
- @NonNull
- private static StringBuilder commonCacheKeyOf(@NonNull final StreamInfo info,
- @NonNull final Stream stream,
+
+ //region Cache key generation
+ private static StringBuilder commonCacheKeyOf(final StreamInfo info,
+ final Stream stream,
final boolean resolutionOrBitrateUnknown) {
// stream info service id
final StringBuilder cacheKey = new StringBuilder(info.getServiceId());
@@ -91,9 +96,20 @@ private static StringBuilder commonCacheKeyOf(@NonNull final StreamInfo info,
return cacheKey;
}
- @NonNull
- static String cacheKeyOf(@NonNull final StreamInfo info,
- @NonNull final VideoStream videoStream) {
+ /**
+ * Builds the cache key of a video stream. A cache key is unique to the features of the
+ * provided video stream, and when possible independent of transient parameters (such as
+ * the url of the stream). This ensures that there are no conflicts, but also that the cache is
+ * used as much as possible: the same cache should be used for two streams which have the same
+ * features but e.g. a different url, since the url might have been reloaded in the meantime,
+ * but the stream actually referenced by the url is still the same.
+ *
+ * @param info the stream info, to distinguish between streams with the same features but coming
+ * from different stream infos
+ * @param videoStream the video stream for which the cache key should be created
+ * @return a key to be used to store the cache of the provided video stream
+ */
+ static String cacheKeyOf(final StreamInfo info, final VideoStream videoStream) {
final boolean resolutionUnknown = videoStream.getResolution().equals(RESOLUTION_UNKNOWN);
final StringBuilder cacheKey = commonCacheKeyOf(info, videoStream, resolutionUnknown);
@@ -110,9 +126,20 @@ static String cacheKeyOf(@NonNull final StreamInfo info,
return cacheKey.toString();
}
- @NonNull
- static String cacheKeyOf(@NonNull final StreamInfo info,
- @NonNull final AudioStream audioStream) {
+ /**
+ * Builds the cache key of an audio stream. A cache key is unique to the features of the
+ * provided audio stream, and when possible independent of transient parameters (such as
+ * the url of the stream). This ensures that there are no conflicts, but also that the cache is
+ * used as much as possible: the same cache should be used for two streams which have the same
+ * features but e.g. a different url, since the url might have been reloaded in the meantime,
+ * but the stream actually referenced by the url is still the same.
+ *
+ * @param info the stream info, to distinguish between streams with the same features but coming
+ * from different stream infos
+ * @param audioStream the audio stream for which the cache key should be created
+ * @return a key to be used to store the cache of the provided audio stream
+ */
+ static String cacheKeyOf(final StreamInfo info, final AudioStream audioStream) {
final boolean averageBitrateUnknown = audioStream.getAverageBitrate() == UNKNOWN_BITRATE;
final StringBuilder cacheKey = commonCacheKeyOf(info, audioStream, averageBitrateUnknown);
@@ -124,10 +151,13 @@ static String cacheKeyOf(@NonNull final StreamInfo info,
return cacheKey.toString();
}
+ //endregion
+
+ //region Live media sources
@Nullable
- static MediaSource maybeBuildLiveMediaSource(@NonNull final PlayerDataSource dataSource,
- @NonNull final StreamInfo info) {
+ static MediaSource maybeBuildLiveMediaSource(final PlayerDataSource dataSource,
+ final StreamInfo info) {
final StreamType streamType = info.getStreamType();
if (!StreamTypeUtil.isLiveStream(streamType)) {
return null;
@@ -143,11 +173,10 @@ static MediaSource maybeBuildLiveMediaSource(@NonNull final PlayerDataSource dat
return null;
}
- @NonNull
- static MediaSource buildLiveMediaSource(@NonNull final PlayerDataSource dataSource,
- @NonNull final String sourceUrl,
+ static MediaSource buildLiveMediaSource(final PlayerDataSource dataSource,
+ final String sourceUrl,
@C.ContentType final int type,
- @NonNull final MediaItemTag metadata) {
+ final MediaItemTag metadata) {
final MediaSource.Factory factory;
switch (type) {
case C.TYPE_SS:
@@ -159,7 +188,7 @@ static MediaSource buildLiveMediaSource(@NonNull final PlayerDataSource dataSour
case C.TYPE_HLS:
factory = dataSource.getLiveHlsMediaSourceFactory();
break;
- default:
+ case C.TYPE_OTHER: case C.TYPE_RTSP: default:
throw new IllegalStateException("Unsupported type: " + type);
}
@@ -173,13 +202,15 @@ static MediaSource buildLiveMediaSource(@NonNull final PlayerDataSource dataSour
.build())
.build());
}
+ //endregion
- @NonNull
- static MediaSource buildMediaSource(@NonNull final PlayerDataSource dataSource,
- @NonNull final Stream stream,
- @NonNull final StreamInfo streamInfo,
- @NonNull final String cacheKey,
- @NonNull final MediaItemTag metadata)
+
+ //region Generic media sources
+ static MediaSource buildMediaSource(final PlayerDataSource dataSource,
+ final Stream stream,
+ final StreamInfo streamInfo,
+ final String cacheKey,
+ final MediaItemTag metadata)
throws IOException {
if (streamInfo.getService() == ServiceList.YouTube) {
return createYoutubeMediaSource(stream, streamInfo, dataSource, cacheKey, metadata);
@@ -201,12 +232,11 @@ static MediaSource buildMediaSource(@NonNull final PlayerDataSource dataSource,
}
}
- @NonNull
- private static ProgressiveMediaSource buildProgressiveMediaSource(
- @NonNull final PlayerDataSource dataSource,
- @NonNull final T stream,
- @NonNull final String cacheKey,
- @NonNull final MediaItemTag metadata) throws IOException {
+ private static ProgressiveMediaSource buildProgressiveMediaSource(
+ final PlayerDataSource dataSource,
+ final Stream stream,
+ final String cacheKey,
+ final MediaItemTag metadata) throws IOException {
final String url = stream.getContent();
if (isNullOrEmpty(url)) {
@@ -223,12 +253,11 @@ private static ProgressiveMediaSource buildProgressiveMediaSo
}
}
- @NonNull
- private static DashMediaSource buildDashMediaSource(
- @NonNull final PlayerDataSource dataSource,
- @NonNull final T stream,
- @NonNull final String cacheKey,
- @NonNull final MediaItemTag metadata) throws IOException {
+ private static DashMediaSource buildDashMediaSource(final PlayerDataSource dataSource,
+ final Stream stream,
+ final String cacheKey,
+ final MediaItemTag metadata)
+ throws IOException {
final boolean isUrlStream = stream.isUrl();
if (isUrlStream && isNullOrEmpty(stream.getContent())) {
throw new IOException("Try to generate a DASH media source from an empty string or "
@@ -260,10 +289,8 @@ private static DashMediaSource buildDashMediaSource(
}
}
- @NonNull
- private static DashManifest createDashManifest(
- @NonNull final String manifestContent,
- @NonNull final T stream) throws IOException {
+ private static DashManifest createDashManifest(final String manifestContent,
+ final Stream stream) throws IOException {
try {
final ByteArrayInputStream dashManifestInput = new ByteArrayInputStream(
manifestContent.getBytes(StandardCharsets.UTF_8));
@@ -278,12 +305,11 @@ private static DashManifest createDashManifest(
}
}
- @NonNull
- private static HlsMediaSource buildHlsMediaSource(
- @NonNull final PlayerDataSource dataSource,
- @NonNull final T stream,
- @NonNull final String cacheKey,
- @NonNull final MediaItemTag metadata) throws IOException {
+ private static HlsMediaSource buildHlsMediaSource(final PlayerDataSource dataSource,
+ final Stream stream,
+ final String cacheKey,
+ final MediaItemTag metadata)
+ throws IOException {
final boolean isUrlStream = stream.isUrl();
if (isUrlStream && isNullOrEmpty(stream.getContent())) {
throw new IOException("Try to generate an HLS media source from an empty string or "
@@ -324,12 +350,11 @@ private static HlsMediaSource buildHlsMediaSource(
}
}
- @NonNull
- private static SsMediaSource buildSSMediaSource(
- @NonNull final PlayerDataSource dataSource,
- @NonNull final T stream,
- @NonNull final String cacheKey,
- @NonNull final MediaItemTag metadata) throws IOException {
+ private static SsMediaSource buildSSMediaSource(final PlayerDataSource dataSource,
+ final Stream stream,
+ final String cacheKey,
+ final MediaItemTag metadata)
+ throws IOException {
final boolean isUrlStream = stream.isUrl();
if (isUrlStream && isNullOrEmpty(stream.getContent())) {
throw new IOException("Try to generate an SmoothStreaming media source from an empty "
@@ -370,13 +395,16 @@ private static SsMediaSource buildSSMediaSource(
.build());
}
}
+ //endregion
- private static MediaSource createYoutubeMediaSource(
- final T stream,
- final StreamInfo streamInfo,
- final PlayerDataSource dataSource,
- final String cacheKey,
- final MediaItemTag metadata) throws IOException {
+
+ //region YouTube media sources
+ private static MediaSource createYoutubeMediaSource(final Stream stream,
+ final StreamInfo streamInfo,
+ final PlayerDataSource dataSource,
+ final String cacheKey,
+ final MediaItemTag metadata)
+ throws IOException {
if (!(stream instanceof AudioStream || stream instanceof VideoStream)) {
throw new IOException("Try to generate a DASH manifest of a YouTube "
+ stream.getClass() + " " + stream.getContent());
@@ -414,12 +442,12 @@ private static MediaSource createYoutubeMediaSource(
}
}
- private static MediaSource createYoutubeMediaSourceOfVideoStreamType(
- @NonNull final PlayerDataSource dataSource,
- @NonNull final T stream,
- @NonNull final StreamInfo streamInfo,
- @NonNull final String cacheKey,
- @NonNull final MediaItemTag metadata) throws IOException {
+ private static MediaSource createYoutubeMediaSourceOfVideoStreamType(
+ final PlayerDataSource dataSource,
+ final Stream stream,
+ final StreamInfo streamInfo,
+ final String cacheKey,
+ final MediaItemTag metadata) throws IOException {
final DeliveryMethod deliveryMethod = stream.getDeliveryMethod();
switch (deliveryMethod) {
case PROGRESSIVE_HTTP:
@@ -480,13 +508,12 @@ private static MediaSource createYoutubeMediaSourceOfVideoStr
}
}
- @NonNull
- private static DashMediaSource buildYoutubeManualDashMediaSource(
- @NonNull final PlayerDataSource dataSource,
- @NonNull final DashManifest dashManifest,
- @NonNull final T stream,
- @NonNull final String cacheKey,
- @NonNull final MediaItemTag metadata) {
+ private static DashMediaSource buildYoutubeManualDashMediaSource(
+ final PlayerDataSource dataSource,
+ final DashManifest dashManifest,
+ final Stream stream,
+ final String cacheKey,
+ final MediaItemTag metadata) {
return dataSource.getYoutubeDashMediaSourceFactory().createMediaSource(dashManifest,
new MediaItem.Builder()
.setTag(metadata)
@@ -495,12 +522,11 @@ private static DashMediaSource buildYoutubeManualDashMediaSou
.build());
}
- @NonNull
- private static ProgressiveMediaSource buildYoutubeProgressiveMediaSource(
- @NonNull final PlayerDataSource dataSource,
- @NonNull final T stream,
- @NonNull final String cacheKey,
- @NonNull final MediaItemTag metadata) {
+ private static ProgressiveMediaSource buildYoutubeProgressiveMediaSource(
+ final PlayerDataSource dataSource,
+ final Stream stream,
+ final String cacheKey,
+ final MediaItemTag metadata) {
return dataSource.getYoutubeProgressiveMediaSourceFactory()
.createMediaSource(new MediaItem.Builder()
.setTag(metadata)
@@ -508,4 +534,5 @@ private static ProgressiveMediaSource buildYoutubeProgressive
.setCustomCacheKey(cacheKey)
.build());
}
+ //endregion
}
From 8dad6d7e1cace27428e71729d2b8cbf26ef1c5f1 Mon Sep 17 00:00:00 2001
From: Stypox
Date: Sat, 21 May 2022 12:02:57 +0200
Subject: [PATCH 074/240] Code improvements here and there
---
.../newpipe/download/DownloadDialog.java | 6 +--
.../newpipe/util/SecondaryStreamHelper.java | 46 ++++++++++---------
.../newpipe/util/StreamItemAdapter.java | 6 +--
3 files changed, 29 insertions(+), 29 deletions(-)
diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java
index 73ba8c74a7b..9f46f7f6bf2 100644
--- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java
+++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java
@@ -786,10 +786,8 @@ private void prepareSelectedDownload() {
if (format == MediaFormat.TTML) {
filenameTmp += MediaFormat.SRT.suffix;
- } else {
- if (format != null) {
- filenameTmp += format.suffix;
- }
+ } else if (format != null) {
+ filenameTmp += format.suffix;
}
break;
default:
diff --git a/app/src/main/java/org/schabi/newpipe/util/SecondaryStreamHelper.java b/app/src/main/java/org/schabi/newpipe/util/SecondaryStreamHelper.java
index 96124da8744..e7fd2d4a4bc 100644
--- a/app/src/main/java/org/schabi/newpipe/util/SecondaryStreamHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/util/SecondaryStreamHelper.java
@@ -35,33 +35,35 @@ public SecondaryStreamHelper(@NonNull final StreamSizeWrapper streams,
public static AudioStream getAudioStreamFor(@NonNull final List audioStreams,
@NonNull final VideoStream videoStream) {
final MediaFormat mediaFormat = videoStream.getFormat();
- if (mediaFormat != null) {
- switch (mediaFormat) {
- case WEBM:
- case MPEG_4:// ¿is mpeg-4 DASH?
- break;
- default:
- return null;
- }
+ if (mediaFormat == null) {
+ return null;
+ }
- final boolean m4v = (mediaFormat == MediaFormat.MPEG_4);
+ switch (mediaFormat) {
+ case WEBM:
+ case MPEG_4:// ¿is mpeg-4 DASH?
+ break;
+ default:
+ return null;
+ }
- for (final AudioStream audio : audioStreams) {
- if (audio.getFormat() == (m4v ? MediaFormat.M4A : MediaFormat.WEBMA)) {
- return audio;
- }
- }
+ final boolean m4v = (mediaFormat == MediaFormat.MPEG_4);
- if (m4v) {
- return null;
+ for (final AudioStream audio : audioStreams) {
+ if (audio.getFormat() == (m4v ? MediaFormat.M4A : MediaFormat.WEBMA)) {
+ return audio;
}
+ }
+
+ if (m4v) {
+ return null;
+ }
- // retry, but this time in reverse order
- for (int i = audioStreams.size() - 1; i >= 0; i--) {
- final AudioStream audio = audioStreams.get(i);
- if (audio.getFormat() == MediaFormat.WEBMA_OPUS) {
- return audio;
- }
+ // retry, but this time in reverse order
+ for (int i = audioStreams.size() - 1; i >= 0; i--) {
+ final AudioStream audio = audioStreams.get(i);
+ if (audio.getFormat() == MediaFormat.WEBMA_OPUS) {
+ return audio;
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java b/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java
index 11f982921fc..4b5e675c917 100644
--- a/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java
+++ b/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java
@@ -155,10 +155,10 @@ private View getCustomView(final int position,
qualityString += " (" + context.getString(R.string.caption_auto_generated) + ")";
}
} else {
- if (mediaFormat != null) {
- qualityString = mediaFormat.getSuffix();
- } else {
+ if (mediaFormat == null) {
qualityString = context.getString(R.string.unknown_quality);
+ } else {
+ qualityString = mediaFormat.getSuffix();
}
}
From 73855cacb730b77a987095a8b79a502a2b6adeb2 Mon Sep 17 00:00:00 2001
From: AudricV <74829229+AudricV@users.noreply.github.com>
Date: Thu, 16 Jun 2022 11:13:54 +0200
Subject: [PATCH 075/240] Use StreamTypeUtil where possible and add isAudio and
isVideo to this utility class
---
.../newpipe/database/stream/dao/StreamDAO.kt | 6 ++--
.../info_list/dialog/InfoItemDialog.java | 8 ++---
.../holder/StreamMiniInfoItemHolder.java | 8 ++---
.../org/schabi/newpipe/player/Player.java | 21 ++++---------
.../schabi/newpipe/util/SparseItemUtil.java | 6 ++--
.../schabi/newpipe/util/StreamTypeUtil.java | 30 +++++++++++++++++--
6 files changed, 44 insertions(+), 35 deletions(-)
diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.kt b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.kt
index a22fd2bb98d..d8c19c1e979 100644
--- a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.kt
+++ b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.kt
@@ -12,8 +12,7 @@ import org.schabi.newpipe.database.BasicDAO
import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_ID
import org.schabi.newpipe.extractor.stream.StreamType
-import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_LIVE_STREAM
-import org.schabi.newpipe.extractor.stream.StreamType.LIVE_STREAM
+import org.schabi.newpipe.util.StreamTypeUtil
import java.time.OffsetDateTime
@Dao
@@ -91,8 +90,7 @@ abstract class StreamDAO : BasicDAO {
?: throw IllegalStateException("Stream cannot be null just after insertion.")
newerStream.uid = existentMinimalStream.uid
- val isNewerStreamLive = newerStream.streamType == AUDIO_LIVE_STREAM || newerStream.streamType == LIVE_STREAM
- if (!isNewerStreamLive) {
+ if (!StreamTypeUtil.isLiveStream(newerStream.streamType)) {
// Use the existent upload date if the newer stream does not have a better precision
// (i.e. is an approximation). This is done to prevent unnecessary changes.
diff --git a/app/src/main/java/org/schabi/newpipe/info_list/dialog/InfoItemDialog.java b/app/src/main/java/org/schabi/newpipe/info_list/dialog/InfoItemDialog.java
index 5a266c0a860..5afaea0384a 100644
--- a/app/src/main/java/org/schabi/newpipe/info_list/dialog/InfoItemDialog.java
+++ b/app/src/main/java/org/schabi/newpipe/info_list/dialog/InfoItemDialog.java
@@ -24,6 +24,7 @@
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.player.helper.PlayerHolder;
+import org.schabi.newpipe.util.StreamTypeUtil;
import org.schabi.newpipe.util.external_communication.KoreUtils;
import java.util.ArrayList;
@@ -269,8 +270,7 @@ public Builder addEnqueueEntriesIfNeeded() {
*/
public Builder addStartHereEntries() {
addEntry(StreamDialogDefaultEntry.START_HERE_ON_BACKGROUND);
- if (infoItem.getStreamType() != StreamType.AUDIO_STREAM
- && infoItem.getStreamType() != StreamType.AUDIO_LIVE_STREAM) {
+ if (!StreamTypeUtil.isAudio(infoItem.getStreamType())) {
addEntry(StreamDialogDefaultEntry.START_HERE_ON_POPUP);
}
return this;
@@ -285,9 +285,7 @@ public Builder addMarkAsWatchedEntryIfNeeded() {
final boolean isWatchHistoryEnabled = PreferenceManager
.getDefaultSharedPreferences(context)
.getBoolean(context.getString(R.string.enable_watch_history_key), false);
- if (isWatchHistoryEnabled
- && infoItem.getStreamType() != StreamType.LIVE_STREAM
- && infoItem.getStreamType() != StreamType.AUDIO_LIVE_STREAM) {
+ if (isWatchHistoryEnabled && !StreamTypeUtil.isLiveStream(infoItem.getStreamType())) {
addEntry(StreamDialogDefaultEntry.MARK_AS_WATCHED);
}
return this;
diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java
index 83211d4dd02..54d31ca5735 100644
--- a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java
+++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java
@@ -11,12 +11,12 @@
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
-import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.ktx.ViewUtils;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.Localization;
+import org.schabi.newpipe.util.StreamTypeUtil;
import org.schabi.newpipe.views.AnimatedProgressBar;
import java.util.concurrent.TimeUnit;
@@ -70,8 +70,7 @@ public void updateFromItem(final InfoItem infoItem,
} else {
itemProgressView.setVisibility(View.GONE);
}
- } else if (item.getStreamType() == StreamType.LIVE_STREAM
- || item.getStreamType() == StreamType.AUDIO_LIVE_STREAM) {
+ } else if (StreamTypeUtil.isLiveStream(item.getStreamType())) {
itemDurationView.setText(R.string.duration_live);
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(),
R.color.live_duration_background_color));
@@ -115,8 +114,7 @@ public void updateState(final InfoItem infoItem,
final StreamStateEntity state
= historyRecordManager.loadStreamState(infoItem).blockingGet()[0];
if (state != null && item.getDuration() > 0
- && item.getStreamType() != StreamType.LIVE_STREAM
- && item.getStreamType() != StreamType.AUDIO_LIVE_STREAM) {
+ && !StreamTypeUtil.isLiveStream(item.getStreamType())) {
itemProgressView.setMax((int) item.getDuration());
if (itemProgressView.getVisibility() == View.VISIBLE) {
itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS
diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java
index 316b72a0925..b2c8836e591 100644
--- a/app/src/main/java/org/schabi/newpipe/player/Player.java
+++ b/app/src/main/java/org/schabi/newpipe/player/Player.java
@@ -4234,10 +4234,7 @@ private void useVideoSource(final boolean videoEnabled) {
if (playQueueManagerReloadingNeeded(sourceType, info, getVideoRendererIndex())) {
reloadPlayQueueManager();
} else {
- final StreamType streamType = info.getStreamType();
- if (streamType == StreamType.AUDIO_STREAM
- || streamType == StreamType.AUDIO_LIVE_STREAM
- || streamType == StreamType.POST_LIVE_AUDIO_STREAM) {
+ if (StreamTypeUtil.isAudio(info.getStreamType())) {
// Nothing to do more than setting the recovery position
setRecovery();
return;
@@ -4296,21 +4293,17 @@ private boolean playQueueManagerReloadingNeeded(final SourceType sourceType,
@NonNull final StreamInfo streamInfo,
final int videoRendererIndex) {
final StreamType streamType = streamInfo.getStreamType();
+ final boolean isStreamTypeAudio = StreamTypeUtil.isAudio(streamType);
- if (videoRendererIndex == RENDERER_UNAVAILABLE && streamType != StreamType.AUDIO_STREAM
- && streamType != StreamType.AUDIO_LIVE_STREAM
- && streamType != StreamType.POST_LIVE_AUDIO_STREAM) {
+ if (videoRendererIndex == RENDERER_UNAVAILABLE && !isStreamTypeAudio) {
return true;
}
// The content is an audio stream, an audio live stream, or a live stream with a live
// source: it's not needed to reload the play queue manager because the stream source will
// be the same
- if ((streamType == StreamType.AUDIO_STREAM
- || streamType == StreamType.POST_LIVE_AUDIO_STREAM
- || streamType == StreamType.AUDIO_LIVE_STREAM)
- || (streamType == StreamType.LIVE_STREAM
- && sourceType == SourceType.LIVE_STREAM)) {
+ if (isStreamTypeAudio || (streamType == StreamType.LIVE_STREAM
+ && sourceType == SourceType.LIVE_STREAM)) {
return false;
}
@@ -4324,9 +4317,7 @@ private boolean playQueueManagerReloadingNeeded(final SourceType sourceType,
&& 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 streamType != StreamType.VIDEO_STREAM
- && streamType != StreamType.LIVE_STREAM
- && streamType != StreamType.POST_LIVE_STREAM;
+ return !StreamTypeUtil.isVideo(streamType);
}
// Other cases: the play queue manager reload is needed
diff --git a/app/src/main/java/org/schabi/newpipe/util/SparseItemUtil.java b/app/src/main/java/org/schabi/newpipe/util/SparseItemUtil.java
index b8cd4ef6903..0c5f418b294 100644
--- a/app/src/main/java/org/schabi/newpipe/util/SparseItemUtil.java
+++ b/app/src/main/java/org/schabi/newpipe/util/SparseItemUtil.java
@@ -1,7 +1,5 @@
package org.schabi.newpipe.util;
-import static org.schabi.newpipe.extractor.stream.StreamType.AUDIO_LIVE_STREAM;
-import static org.schabi.newpipe.extractor.stream.StreamType.LIVE_STREAM;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
import android.content.Context;
@@ -49,8 +47,8 @@ private SparseItemUtil() {
public static void fetchItemInfoIfSparse(@NonNull final Context context,
@NonNull final StreamInfoItem item,
@NonNull final Consumer callback) {
- if (((item.getStreamType() == LIVE_STREAM || item.getStreamType() == AUDIO_LIVE_STREAM)
- || item.getDuration() >= 0) && !isNullOrEmpty(item.getUploaderUrl())) {
+ if ((StreamTypeUtil.isLiveStream(item.getStreamType()) || item.getDuration() >= 0)
+ && !isNullOrEmpty(item.getUploaderUrl())) {
// if the duration is >= 0 (provided that the item is not a livestream) and there is an
// uploader url, probably all info is already there, so there is no need to fetch it
callback.accept(new SinglePlayQueue(item));
diff --git a/app/src/main/java/org/schabi/newpipe/util/StreamTypeUtil.java b/app/src/main/java/org/schabi/newpipe/util/StreamTypeUtil.java
index b0b6f4507ef..0cc0ecf1fd3 100644
--- a/app/src/main/java/org/schabi/newpipe/util/StreamTypeUtil.java
+++ b/app/src/main/java/org/schabi/newpipe/util/StreamTypeUtil.java
@@ -14,8 +14,34 @@ private StreamTypeUtil() {
* Check if the {@link StreamType} of a stream is a livestream.
*
* @param streamType the stream type of the stream
- * @return true if the streamType is a
- * {@link StreamType#LIVE_STREAM} or {@link StreamType#AUDIO_LIVE_STREAM}
+ * @return whether the stream type is {@link StreamType#AUDIO_STREAM},
+ * {@link StreamType#AUDIO_LIVE_STREAM} or {@link StreamType#POST_LIVE_AUDIO_STREAM}
+ */
+ public static boolean isAudio(final StreamType streamType) {
+ return streamType == StreamType.AUDIO_STREAM
+ || streamType == StreamType.AUDIO_LIVE_STREAM
+ || streamType == StreamType.POST_LIVE_AUDIO_STREAM;
+ }
+
+ /**
+ * Check if the {@link StreamType} of a stream is a livestream.
+ *
+ * @param streamType the stream type of the stream
+ * @return whether the stream type is {@link StreamType#VIDEO_STREAM},
+ * {@link StreamType#LIVE_STREAM} or {@link StreamType#POST_LIVE_STREAM}
+ */
+ public static boolean isVideo(final StreamType streamType) {
+ return streamType == StreamType.VIDEO_STREAM
+ || streamType == StreamType.LIVE_STREAM
+ || streamType == StreamType.POST_LIVE_STREAM;
+ }
+
+ /**
+ * Check if the {@link StreamType} of a stream is a livestream.
+ *
+ * @param streamType the stream type of the stream
+ * @return whether the stream type is {@link StreamType#LIVE_STREAM} or
+ * {@link StreamType#AUDIO_LIVE_STREAM}
*/
public static boolean isLiveStream(final StreamType streamType) {
return streamType == StreamType.LIVE_STREAM
From 036196a48747682bcad3831fae36db2916e9beb0 Mon Sep 17 00:00:00 2001
From: AudricV <74829229+AudricV@users.noreply.github.com>
Date: Thu, 16 Jun 2022 11:14:02 +0200
Subject: [PATCH 076/240] Filter streams using Java 8 Stream's API instead of
removing streams with list iterators and add a better toast when there is no
audio stream for external players
This ensures to not remove streams from the StreamInfo lists themselves, and so to not have to create list copies.
The toast shown in RouterActivity, when there is no audio stream available for external players, is now shown, in the same case, when pressing the background button in VideoDetailFragment.
---
.../newpipe/download/DownloadDialog.java | 24 +++---
.../fragments/detail/VideoDetailFragment.java | 26 ++++--
.../resolver/AudioPlaybackResolver.java | 6 +-
.../resolver/VideoPlaybackResolver.java | 29 +++----
.../org/schabi/newpipe/util/ListHelper.java | 84 ++++++++----------
.../schabi/newpipe/util/NavigationHelper.java | 86 ++++++++++---------
6 files changed, 123 insertions(+), 132 deletions(-)
diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java
index 9f46f7f6bf2..4fb47496bea 100644
--- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java
+++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java
@@ -69,7 +69,6 @@
import java.io.File;
import java.io.IOException;
-import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
@@ -84,7 +83,7 @@
import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder;
import us.shandian.giga.service.MissionState;
-import static org.schabi.newpipe.util.ListHelper.keepStreamsWithDelivery;
+import static org.schabi.newpipe.util.ListHelper.getStreamsOfSpecifiedDelivery;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
public class DownloadDialog extends DialogFragment
@@ -149,25 +148,24 @@ public class DownloadDialog extends DialogFragment
public static DownloadDialog newInstance(final Context context,
@NonNull final StreamInfo info) {
// TODO: Adapt this code when the downloader support other types of stream deliveries
- final List videoStreams = new ArrayList<>(info.getVideoStreams());
final List progressiveHttpVideoStreams =
- keepStreamsWithDelivery(videoStreams, DeliveryMethod.PROGRESSIVE_HTTP);
+ getStreamsOfSpecifiedDelivery(info.getVideoStreams(),
+ DeliveryMethod.PROGRESSIVE_HTTP);
- final List videoOnlyStreams = new ArrayList<>(info.getVideoOnlyStreams());
final List progressiveHttpVideoOnlyStreams =
- keepStreamsWithDelivery(videoOnlyStreams, DeliveryMethod.PROGRESSIVE_HTTP);
+ getStreamsOfSpecifiedDelivery(info.getVideoOnlyStreams(),
+ DeliveryMethod.PROGRESSIVE_HTTP);
- final List audioStreams = new ArrayList<>(info.getAudioStreams());
final List progressiveHttpAudioStreams =
- keepStreamsWithDelivery(audioStreams, DeliveryMethod.PROGRESSIVE_HTTP);
+ getStreamsOfSpecifiedDelivery(info.getAudioStreams(),
+ DeliveryMethod.PROGRESSIVE_HTTP);
- final List subtitlesStreams = new ArrayList<>(info.getSubtitles());
final List progressiveHttpSubtitlesStreams =
- keepStreamsWithDelivery(subtitlesStreams, DeliveryMethod.PROGRESSIVE_HTTP);
+ getStreamsOfSpecifiedDelivery(info.getSubtitles(),
+ DeliveryMethod.PROGRESSIVE_HTTP);
- final List videoStreamsList = new ArrayList<>(
- ListHelper.getSortedStreamVideosList(context, progressiveHttpVideoStreams,
- progressiveHttpVideoOnlyStreams, false, false));
+ final List videoStreamsList = ListHelper.getSortedStreamVideosList(context,
+ progressiveHttpVideoStreams, progressiveHttpVideoOnlyStreams, false, false);
final DownloadDialog instance = new DownloadDialog();
instance.setInfo(info);
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java
index bb09681f537..ff2114b83cb 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java
@@ -31,6 +31,7 @@
import android.view.animation.DecelerateInterpolator;
import android.widget.FrameLayout;
import android.widget.RelativeLayout;
+import android.widget.Toast;
import androidx.annotation.AttrRes;
import androidx.annotation.NonNull;
@@ -122,7 +123,7 @@
import static org.schabi.newpipe.player.helper.PlayerHelper.isClearingQueueConfirmationRequired;
import static org.schabi.newpipe.player.playqueue.PlayQueueItem.RECOVERY_UNSET;
import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
-import static org.schabi.newpipe.util.ListHelper.removeNonUrlAndTorrentStreams;
+import static org.schabi.newpipe.util.ListHelper.getNonUrlAndNonTorrentStreams;
public final class VideoDetailFragment
extends BaseStateFragment
@@ -1092,9 +1093,6 @@ private void toggleFullscreenIfInFullscreenMode() {
}
private void openBackgroundPlayer(final boolean append) {
- final AudioStream audioStream = currentInfo.getAudioStreams()
- .get(ListHelper.getDefaultAudioFormat(activity, currentInfo.getAudioStreams()));
-
final boolean useExternalAudioPlayer = PreferenceManager
.getDefaultSharedPreferences(activity)
.getBoolean(activity.getString(R.string.use_external_audio_player_key), false);
@@ -1109,7 +1107,17 @@ private void openBackgroundPlayer(final boolean append) {
if (!useExternalAudioPlayer) {
openNormalBackgroundPlayer(append);
} else {
- startOnExternalPlayer(activity, currentInfo, audioStream);
+ final List audioStreams = getNonUrlAndNonTorrentStreams(
+ 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));
}
}
@@ -2147,10 +2155,10 @@ private void showExternalPlaybackDialog() {
return;
}
- final List videoStreams = removeNonUrlAndTorrentStreams(
- new ArrayList<>(currentInfo.getVideoStreams()));
- final List videoOnlyStreams = removeNonUrlAndTorrentStreams(
- new ArrayList<>(currentInfo.getVideoOnlyStreams()));
+ final List videoStreams = getNonUrlAndNonTorrentStreams(
+ currentInfo.getVideoStreams());
+ final List videoOnlyStreams = getNonUrlAndNonTorrentStreams(
+ currentInfo.getVideoOnlyStreams());
final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setTitle(R.string.select_quality_external_players);
diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java
index 85c15faf154..3e166c3398c 100644
--- a/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java
+++ b/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java
@@ -1,6 +1,6 @@
package org.schabi.newpipe.player.resolver;
-import static org.schabi.newpipe.util.ListHelper.removeTorrentStreams;
+import static org.schabi.newpipe.util.ListHelper.getNonTorrentStreams;
import android.content.Context;
import android.util.Log;
@@ -18,7 +18,6 @@
import org.schabi.newpipe.util.ListHelper;
import java.io.IOException;
-import java.util.ArrayList;
import java.util.List;
public class AudioPlaybackResolver implements PlaybackResolver {
@@ -43,8 +42,7 @@ public MediaSource resolve(@NonNull final StreamInfo info) {
return liveSource;
}
- final List audioStreams = new ArrayList<>(info.getAudioStreams());
- removeTorrentStreams(audioStreams);
+ final List audioStreams = getNonTorrentStreams(info.getAudioStreams());
final int index = ListHelper.getDefaultAudioFormat(context, audioStreams);
if (index < 0 || index >= info.getAudioStreams().size()) {
diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java
index 317c49fc93a..fd00d0ed9ab 100644
--- a/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java
+++ b/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java
@@ -29,8 +29,8 @@
import java.util.Optional;
import static com.google.android.exoplayer2.C.TIME_UNSET;
-import static org.schabi.newpipe.util.ListHelper.removeNonUrlAndTorrentStreams;
-import static org.schabi.newpipe.util.ListHelper.removeTorrentStreams;
+import static org.schabi.newpipe.util.ListHelper.getNonUrlAndNonTorrentStreams;
+import static org.schabi.newpipe.util.ListHelper.getNonTorrentStreams;
public class VideoPlaybackResolver implements PlaybackResolver {
private static final String TAG = VideoPlaybackResolver.class.getSimpleName();
@@ -70,24 +70,21 @@ public MediaSource resolve(@NonNull final StreamInfo info) {
}
final List mediaSources = new ArrayList<>();
- final List videoStreams = new ArrayList<>(info.getVideoStreams());
- final List videoOnlyStreams = new ArrayList<>(info.getVideoOnlyStreams());
-
- removeTorrentStreams(videoStreams);
- removeTorrentStreams(videoOnlyStreams);
// Create video stream source
- final List videos = ListHelper.getSortedStreamVideosList(context,
- videoStreams, videoOnlyStreams, false, true);
+ final List videoStreamsList = ListHelper.getSortedStreamVideosList(context,
+ getNonTorrentStreams(info.getVideoStreams()),
+ getNonTorrentStreams(info.getVideoOnlyStreams()), false, true);
final int index;
- if (videos.isEmpty()) {
+ if (videoStreamsList.isEmpty()) {
index = -1;
} else if (playbackQuality == null) {
- index = qualityResolver.getDefaultResolutionIndex(videos);
+ index = qualityResolver.getDefaultResolutionIndex(videoStreamsList);
} else {
- index = qualityResolver.getOverrideResolutionIndex(videos, getPlaybackQuality());
+ index = qualityResolver.getOverrideResolutionIndex(videoStreamsList,
+ getPlaybackQuality());
}
- final MediaItemTag tag = StreamInfoTag.of(info, videos, index);
+ final MediaItemTag tag = StreamInfoTag.of(info, videoStreamsList, index);
@Nullable final VideoStream video = tag.getMaybeQuality()
.map(MediaItemTag.Quality::getSelectedVideoStream)
.orElse(null);
@@ -104,8 +101,7 @@ public MediaSource resolve(@NonNull final StreamInfo info) {
}
// Create optional audio stream source
- final List audioStreams = info.getAudioStreams();
- removeTorrentStreams(audioStreams);
+ final List audioStreams = getNonTorrentStreams(info.getAudioStreams());
final AudioStream audio = audioStreams.isEmpty() ? null : audioStreams.get(
ListHelper.getDefaultAudioFormat(context, audioStreams));
@@ -129,13 +125,14 @@ public MediaSource resolve(@NonNull final StreamInfo info) {
if (mediaSources.isEmpty()) {
return null;
}
+
// Below are auxiliary media sources
// Create subtitle sources
final List subtitlesStreams = info.getSubtitles();
if (subtitlesStreams != null) {
// Torrent and non URL subtitles are not supported by ExoPlayer
- final List nonTorrentAndUrlStreams = removeNonUrlAndTorrentStreams(
+ final List nonTorrentAndUrlStreams = getNonUrlAndNonTorrentStreams(
subtitlesStreams);
for (final SubtitlesStream subtitle : nonTorrentAndUrlStreams) {
final MediaFormat mediaFormat = subtitle.getFormat();
diff --git a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java
index 3a03e0b3023..33c7a2f49b6 100644
--- a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java
@@ -23,10 +23,10 @@
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
-import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Set;
+import java.util.function.Predicate;
import java.util.stream.Collectors;
public final class ListHelper {
@@ -116,27 +116,17 @@ public static int getDefaultAudioFormat(final Context context,
* Return a {@link Stream} list which uses the given delivery method from a {@link Stream}
* list.
*
- * @param streamList the original stream list
- * @param deliveryMethod the delivery method
+ * @param streamList the original {@link Stream stream} list
+ * @param deliveryMethod the {@link DeliveryMethod delivery method}
* @param the item type's class that extends {@link Stream}
- * @return a stream list which uses the given delivery method
+ * @return a {@link Stream stream} list which uses the given delivery method
*/
@NonNull
- public static List keepStreamsWithDelivery(
- @NonNull final List streamList,
+ public static List getStreamsOfSpecifiedDelivery(
+ final List streamList,
final DeliveryMethod deliveryMethod) {
- if (streamList.isEmpty()) {
- return Collections.emptyList();
- }
-
- final Iterator streamListIterator = streamList.iterator();
- while (streamListIterator.hasNext()) {
- if (streamListIterator.next().getDeliveryMethod() != deliveryMethod) {
- streamListIterator.remove();
- }
- }
-
- return streamList;
+ return getFilteredStreamList(streamList,
+ stream -> stream.getDeliveryMethod() == deliveryMethod);
}
/**
@@ -147,21 +137,10 @@ public static List keepStreamsWithDelivery(
* @return a stream list which only contains URL streams and non-torrent streams
*/
@NonNull
- public static List removeNonUrlAndTorrentStreams(
- @NonNull final List streamList) {
- if (streamList.isEmpty()) {
- return Collections.emptyList();
- }
-
- final Iterator streamListIterator = streamList.iterator();
- while (streamListIterator.hasNext()) {
- final S stream = streamListIterator.next();
- if (!stream.isUrl() || stream.getDeliveryMethod() == DeliveryMethod.TORRENT) {
- streamListIterator.remove();
- }
- }
-
- return streamList;
+ public static List getNonUrlAndNonTorrentStreams(
+ final List streamList) {
+ return getFilteredStreamList(streamList,
+ stream -> stream.isUrl() && stream.getDeliveryMethod() != DeliveryMethod.TORRENT);
}
/**
@@ -172,21 +151,10 @@ public static List removeNonUrlAndTorrentStreams(
* @return a stream list which only contains non-torrent streams
*/
@NonNull
- public static List removeTorrentStreams(
- @NonNull final List streamList) {
- if (streamList.isEmpty()) {
- return Collections.emptyList();
- }
-
- final Iterator streamListIterator = streamList.iterator();
- while (streamListIterator.hasNext()) {
- final S stream = streamListIterator.next();
- if (stream.getDeliveryMethod() == DeliveryMethod.TORRENT) {
- streamListIterator.remove();
- }
- }
-
- return streamList;
+ public static List getNonTorrentStreams(
+ final List streamList) {
+ return getFilteredStreamList(streamList,
+ stream -> stream.getDeliveryMethod() != DeliveryMethod.TORRENT);
}
/**
@@ -224,6 +192,26 @@ public static List getSortedStreamVideosList(
// Utils
//////////////////////////////////////////////////////////////////////////*/
+ /**
+ * Get a filtered stream list, by using Java 8 Stream's API and the given predicate.
+ *
+ * @param streamList the stream list to filter
+ * @param streamListPredicate the predicate which will be used to filter streams
+ * @param the item type's class that extends {@link Stream}
+ * @return a new stream list filtered using the given predicate
+ */
+ private static List getFilteredStreamList(
+ final List streamList,
+ final Predicate streamListPredicate) {
+ if (streamList == null) {
+ return Collections.emptyList();
+ }
+
+ return streamList.stream()
+ .filter(streamListPredicate)
+ .collect(Collectors.toList());
+ }
+
private static String computeDefaultResolution(final Context context, final int key,
final int value) {
final SharedPreferences preferences
diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java
index c3246857e1c..ffc7433a0ec 100644
--- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java
@@ -63,7 +63,7 @@
import java.util.List;
-import static org.schabi.newpipe.util.ListHelper.removeNonUrlAndTorrentStreams;
+import static org.schabi.newpipe.util.ListHelper.getNonUrlAndNonTorrentStreams;
public final class NavigationHelper {
public static final String MAIN_FRAGMENT_TAG = "main_fragment_tag";
@@ -221,39 +221,42 @@ public static void enqueueNextOnPlayer(final Context context, final PlayQueue qu
public static void playOnExternalAudioPlayer(@NonNull final Context context,
@NonNull final StreamInfo info) {
final List audioStreams = info.getAudioStreams();
- if (audioStreams.isEmpty()) {
+ if (audioStreams == null || audioStreams.isEmpty()) {
Toast.makeText(context, R.string.audio_streams_empty, Toast.LENGTH_SHORT).show();
return;
}
- final List audioStreamsForExternalPlayers = removeNonUrlAndTorrentStreams(
- audioStreams);
+
+ final List audioStreamsForExternalPlayers =
+ getNonUrlAndNonTorrentStreams(audioStreams);
if (audioStreamsForExternalPlayers.isEmpty()) {
Toast.makeText(context, R.string.no_audio_streams_available_for_external_players,
Toast.LENGTH_SHORT).show();
return;
}
- final int index = ListHelper.getDefaultAudioFormat(context,
- audioStreamsForExternalPlayers);
+ final int index = ListHelper.getDefaultAudioFormat(context, audioStreamsForExternalPlayers);
final AudioStream audioStream = audioStreamsForExternalPlayers.get(index);
+
playOnExternalPlayer(context, info.getName(), info.getUploaderName(), audioStream);
}
public static void playOnExternalVideoPlayer(final Context context,
@NonNull final StreamInfo info) {
final List videoStreams = info.getVideoStreams();
- if (videoStreams.isEmpty()) {
+ if (videoStreams == null || videoStreams.isEmpty()) {
Toast.makeText(context, R.string.video_streams_empty, Toast.LENGTH_SHORT).show();
return;
}
+
final List videoStreamsForExternalPlayers =
ListHelper.getSortedStreamVideosList(context,
- removeNonUrlAndTorrentStreams(videoStreams), null, false, false);
+ getNonUrlAndNonTorrentStreams(videoStreams), null, false, false);
if (videoStreamsForExternalPlayers.isEmpty()) {
Toast.makeText(context, R.string.no_video_streams_available_for_external_players,
Toast.LENGTH_SHORT).show();
return;
}
+
final int index = ListHelper.getDefaultResolutionIndex(context,
videoStreamsForExternalPlayers);
@@ -267,42 +270,41 @@ public static void playOnExternalPlayer(@NonNull final Context context,
@NonNull final Stream stream) {
final DeliveryMethod deliveryMethod = stream.getDeliveryMethod();
final String mimeType;
- if (deliveryMethod == DeliveryMethod.PROGRESSIVE_HTTP) {
- if (stream.getFormat() != null) {
- mimeType = stream.getFormat().getMimeType();
- } else {
- if (stream instanceof AudioStream) {
- mimeType = "audio/*";
- } else if (stream instanceof VideoStream) {
- mimeType = "video/*";
+
+ if (!stream.isUrl() || deliveryMethod == DeliveryMethod.TORRENT) {
+ Toast.makeText(context, R.string.selected_stream_external_player_not_supported,
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ switch (deliveryMethod) {
+ case PROGRESSIVE_HTTP:
+ if (stream.getFormat() == null) {
+ if (stream instanceof AudioStream) {
+ mimeType = "audio/*";
+ } else if (stream instanceof VideoStream) {
+ mimeType = "video/*";
+ } else {
+ // This should never be reached, because subtitles are not opened in
+ // external players
+ return;
+ }
} else {
- // This should never be reached, because subtitles are not opened in external
- // players
- return;
+ mimeType = stream.getFormat().getMimeType();
}
- }
- } else {
- if (!stream.isUrl() || deliveryMethod == DeliveryMethod.TORRENT) {
- Toast.makeText(context, R.string.selected_stream_external_player_not_supported,
- Toast.LENGTH_SHORT).show();
- return;
- } else {
- switch (deliveryMethod) {
- case HLS:
- mimeType = "application/x-mpegURL";
- break;
- case DASH:
- mimeType = "application/dash+xml";
- break;
- case SS:
- mimeType = "application/vnd.ms-sstr+xml";
- break;
- default:
- // Progressive HTTP streams are handled above and torrents streams are not
- // exposed to external players
- mimeType = "";
- }
- }
+ break;
+ case HLS:
+ mimeType = "application/x-mpegURL";
+ break;
+ case DASH:
+ mimeType = "application/dash+xml";
+ break;
+ case SS:
+ mimeType = "application/vnd.ms-sstr+xml";
+ break;
+ default:
+ // Torrent streams are not exposed to external players
+ mimeType = "";
}
final Intent intent = new Intent();
From 21c9530e8b9d44dd49260649184fe8cc8f200d97 Mon Sep 17 00:00:00 2001
From: AudricV <74829229+AudricV@users.noreply.github.com>
Date: Thu, 16 Jun 2022 11:14:08 +0200
Subject: [PATCH 077/240] Throw a dedicated exception when errors occur in
PlaybackResolver
A new exception, ResolverException, a subclass of PlaybackResolver, is now thrown when errors occur in PlaybackResolver, instead of an IOException
---
.../resolver/AudioPlaybackResolver.java | 5 +-
.../player/resolver/PlaybackResolver.java | 184 ++++++++++--------
.../resolver/VideoPlaybackResolver.java | 9 +-
3 files changed, 112 insertions(+), 86 deletions(-)
diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java
index 3e166c3398c..934beba196f 100644
--- a/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java
+++ b/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java
@@ -17,7 +17,6 @@
import org.schabi.newpipe.player.mediaitem.StreamInfoTag;
import org.schabi.newpipe.util.ListHelper;
-import java.io.IOException;
import java.util.List;
public class AudioPlaybackResolver implements PlaybackResolver {
@@ -55,8 +54,8 @@ public MediaSource resolve(@NonNull final StreamInfo info) {
try {
return PlaybackResolver.buildMediaSource(
dataSource, audio, info, PlaybackResolver.cacheKeyOf(info, audio), tag);
- } catch (final IOException e) {
- Log.e(TAG, "Unable to create audio source:", e);
+ } catch (final ResolverException e) {
+ Log.e(TAG, "Unable to create audio source", e);
return null;
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java
index 3d11f0e448b..d7f04774c89 100644
--- a/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java
+++ b/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java
@@ -97,17 +97,22 @@ private static StringBuilder commonCacheKeyOf(final StreamInfo info,
}
/**
- * Builds the cache key of a video stream. A cache key is unique to the features of the
- * provided video stream, and when possible independent of transient parameters (such as
- * the url of the stream). This ensures that there are no conflicts, but also that the cache is
- * used as much as possible: the same cache should be used for two streams which have the same
- * features but e.g. a different url, since the url might have been reloaded in the meantime,
- * but the stream actually referenced by the url is still the same.
+ * Builds the cache key of a {@link VideoStream video stream}.
*
- * @param info the stream info, to distinguish between streams with the same features but coming
- * from different stream infos
- * @param videoStream the video stream for which the cache key should be created
- * @return a key to be used to store the cache of the provided video stream
+ *
+ * A cache key is unique to the features of the provided video stream, and when possible
+ * independent of transient parameters (such as the URL of the stream).
+ * This ensures that there are no conflicts, but also that the cache is used as much as
+ * possible: the same cache should be used for two streams which have the same features but
+ * e.g. a different URL, since the URL might have been reloaded in the meantime, but the stream
+ * actually referenced by the URL is still the same.
+ *
+ *
+ * @param info the {@link StreamInfo stream info}, to distinguish between streams with
+ * the same features but coming from different stream infos
+ * @param videoStream the {@link VideoStream video stream} for which the cache key should be
+ * created
+ * @return a key to be used to store the cache of the provided {@link VideoStream video stream}
*/
static String cacheKeyOf(final StreamInfo info, final VideoStream videoStream) {
final boolean resolutionUnknown = videoStream.getResolution().equals(RESOLUTION_UNKNOWN);
@@ -127,17 +132,22 @@ static String cacheKeyOf(final StreamInfo info, final VideoStream videoStream) {
}
/**
- * Builds the cache key of an audio stream. A cache key is unique to the features of the
- * provided audio stream, and when possible independent of transient parameters (such as
- * the url of the stream). This ensures that there are no conflicts, but also that the cache is
- * used as much as possible: the same cache should be used for two streams which have the same
- * features but e.g. a different url, since the url might have been reloaded in the meantime,
- * but the stream actually referenced by the url is still the same.
+ * Builds the cache key of an audio stream.
+ *
+ *
+ * A cache key is unique to the features of the provided {@link AudioStream audio stream}, and
+ * when possible independent of transient parameters (such as the URL of the stream).
+ * This ensures that there are no conflicts, but also that the cache is used as much as
+ * possible: the same cache should be used for two streams which have the same features but
+ * e.g. a different URL, since the URL might have been reloaded in the meantime, but the stream
+ * actually referenced by the URL is still the same.
+ *
*
- * @param info the stream info, to distinguish between streams with the same features but coming
- * from different stream infos
- * @param audioStream the audio stream for which the cache key should be created
- * @return a key to be used to store the cache of the provided audio stream
+ * @param info the {@link StreamInfo stream info}, to distinguish between streams with
+ * the same features but coming from different stream infos
+ * @param audioStream the {@link AudioStream audio stream} for which the cache key should be
+ * created
+ * @return a key to be used to store the cache of the provided {@link AudioStream audio stream}
*/
static String cacheKeyOf(final StreamInfo info, final AudioStream audioStream) {
final boolean averageBitrateUnknown = audioStream.getAverageBitrate() == UNKNOWN_BITRATE;
@@ -158,16 +168,20 @@ static String cacheKeyOf(final StreamInfo info, final AudioStream audioStream) {
@Nullable
static MediaSource maybeBuildLiveMediaSource(final PlayerDataSource dataSource,
final StreamInfo info) {
- final StreamType streamType = info.getStreamType();
- if (!StreamTypeUtil.isLiveStream(streamType)) {
+ if (!StreamTypeUtil.isLiveStream(info.getStreamType())) {
return null;
}
- final StreamInfoTag tag = StreamInfoTag.of(info);
- if (!info.getHlsUrl().isEmpty()) {
- return buildLiveMediaSource(dataSource, info.getHlsUrl(), C.TYPE_HLS, tag);
- } else if (!info.getDashMpdUrl().isEmpty()) {
- return buildLiveMediaSource(dataSource, info.getDashMpdUrl(), C.TYPE_DASH, tag);
+ try {
+ final StreamInfoTag tag = StreamInfoTag.of(info);
+ if (!info.getHlsUrl().isEmpty()) {
+ return buildLiveMediaSource(dataSource, info.getHlsUrl(), C.TYPE_HLS, tag);
+ } else if (!info.getDashMpdUrl().isEmpty()) {
+ return buildLiveMediaSource(dataSource, info.getDashMpdUrl(), C.TYPE_DASH, tag);
+ }
+ } catch (final Exception e) {
+ Log.w(TAG, "Error when generating live media source, falling back to standard sources",
+ e);
}
return null;
@@ -176,7 +190,7 @@ static MediaSource maybeBuildLiveMediaSource(final PlayerDataSource dataSource,
static MediaSource buildLiveMediaSource(final PlayerDataSource dataSource,
final String sourceUrl,
@C.ContentType final int type,
- final MediaItemTag metadata) {
+ final MediaItemTag metadata) throws ResolverException {
final MediaSource.Factory factory;
switch (type) {
case C.TYPE_SS:
@@ -188,8 +202,10 @@ static MediaSource buildLiveMediaSource(final PlayerDataSource dataSource,
case C.TYPE_HLS:
factory = dataSource.getLiveHlsMediaSourceFactory();
break;
- case C.TYPE_OTHER: case C.TYPE_RTSP: default:
- throw new IllegalStateException("Unsupported type: " + type);
+ case C.TYPE_OTHER:
+ case C.TYPE_RTSP:
+ default:
+ throw new ResolverException("Unsupported type: " + type);
}
return factory.createMediaSource(
@@ -210,8 +226,7 @@ static MediaSource buildMediaSource(final PlayerDataSource dataSource,
final Stream stream,
final StreamInfo streamInfo,
final String cacheKey,
- final MediaItemTag metadata)
- throws IOException {
+ final MediaItemTag metadata) throws ResolverException {
if (streamInfo.getService() == ServiceList.YouTube) {
return createYoutubeMediaSource(stream, streamInfo, dataSource, cacheKey, metadata);
}
@@ -228,7 +243,7 @@ static MediaSource buildMediaSource(final PlayerDataSource dataSource,
return buildSSMediaSource(dataSource, stream, cacheKey, metadata);
// Torrent streams are not supported by ExoPlayer
default:
- throw new IllegalArgumentException("Unsupported delivery type: " + deliveryMethod);
+ throw new ResolverException("Unsupported delivery type: " + deliveryMethod);
}
}
@@ -236,11 +251,11 @@ private static ProgressiveMediaSource buildProgressiveMediaSource(
final PlayerDataSource dataSource,
final Stream stream,
final String cacheKey,
- final MediaItemTag metadata) throws IOException {
+ final MediaItemTag metadata) throws ResolverException {
final String url = stream.getContent();
if (isNullOrEmpty(url)) {
- throw new IOException(
+ throw new ResolverException(
"Try to generate a progressive media source from an empty string or from a "
+ "null object");
} else {
@@ -257,11 +272,11 @@ private static DashMediaSource buildDashMediaSource(final PlayerDataSource dataS
final Stream stream,
final String cacheKey,
final MediaItemTag metadata)
- throws IOException {
+ throws ResolverException {
final boolean isUrlStream = stream.isUrl();
if (isUrlStream && isNullOrEmpty(stream.getContent())) {
- throw new IOException("Try to generate a DASH media source from an empty string or "
- + "from a null object");
+ throw new ResolverException(
+ "Could not build a DASH media source from an empty or a null URL content");
}
if (isUrlStream) {
@@ -279,41 +294,42 @@ private static DashMediaSource buildDashMediaSource(final PlayerDataSource dataS
final Uri uri = Uri.parse(baseUrl);
- return dataSource.getDashMediaSourceFactory().createMediaSource(
- createDashManifest(stream.getContent(), stream),
- new MediaItem.Builder()
- .setTag(metadata)
- .setUri(uri)
- .setCustomCacheKey(cacheKey)
- .build());
+ try {
+ return dataSource.getDashMediaSourceFactory().createMediaSource(
+ createDashManifest(stream.getContent(), stream),
+ new MediaItem.Builder()
+ .setTag(metadata)
+ .setUri(uri)
+ .setCustomCacheKey(cacheKey)
+ .build());
+ } catch (final IOException e) {
+ throw new ResolverException(
+ "Could not create a DASH media source/manifest from the manifest text");
+ }
}
}
private static DashManifest createDashManifest(final String manifestContent,
final Stream stream) throws IOException {
- try {
- final ByteArrayInputStream dashManifestInput = new ByteArrayInputStream(
- manifestContent.getBytes(StandardCharsets.UTF_8));
- String baseUrl = stream.getManifestUrl();
- if (baseUrl == null) {
- baseUrl = "";
- }
-
- return new DashManifestParser().parse(Uri.parse(baseUrl), dashManifestInput);
- } catch (final IOException e) {
- throw new IOException("Error when parsing manual DASH manifest", e);
+ final ByteArrayInputStream dashManifestInput = new ByteArrayInputStream(
+ manifestContent.getBytes(StandardCharsets.UTF_8));
+ String baseUrl = stream.getManifestUrl();
+ if (baseUrl == null) {
+ baseUrl = "";
}
+
+ return new DashManifestParser().parse(Uri.parse(baseUrl), dashManifestInput);
}
private static HlsMediaSource buildHlsMediaSource(final PlayerDataSource dataSource,
final Stream stream,
final String cacheKey,
final MediaItemTag metadata)
- throws IOException {
+ throws ResolverException {
final boolean isUrlStream = stream.isUrl();
if (isUrlStream && isNullOrEmpty(stream.getContent())) {
- throw new IOException("Try to generate an HLS media source from an empty string or "
- + "from a null object");
+ throw new ResolverException(
+ "Could not build a HLS media source from an empty or a null URL content");
}
if (isUrlStream) {
@@ -337,7 +353,7 @@ private static HlsMediaSource buildHlsMediaSource(final PlayerDataSource dataSou
stream.getContent().getBytes(StandardCharsets.UTF_8));
hlsPlaylist = new HlsPlaylistParser().parse(uri, hlsManifestInput);
} catch (final IOException e) {
- throw new IOException("Error when parsing manual HLS manifest", e);
+ throw new ResolverException("Error when parsing manual HLS manifest", e);
}
return dataSource.getHlsMediaSourceFactory(
@@ -354,11 +370,11 @@ private static SsMediaSource buildSSMediaSource(final PlayerDataSource dataSourc
final Stream stream,
final String cacheKey,
final MediaItemTag metadata)
- throws IOException {
+ throws ResolverException {
final boolean isUrlStream = stream.isUrl();
if (isUrlStream && isNullOrEmpty(stream.getContent())) {
- throw new IOException("Try to generate an SmoothStreaming media source from an empty "
- + "string or from a null object");
+ throw new ResolverException(
+ "Could not build a SS media source from an empty or a null URL content");
}
if (isUrlStream) {
@@ -383,7 +399,7 @@ private static SsMediaSource buildSSMediaSource(final PlayerDataSource dataSourc
smoothStreamingManifest = new SsManifestParser().parse(uri,
smoothStreamingManifestInput);
} catch (final IOException e) {
- throw new IOException("Error when parsing manual SmoothStreaming manifest", e);
+ throw new ResolverException("Error when parsing manual SS manifest", e);
}
return dataSource.getSSMediaSourceFactory().createMediaSource(
@@ -404,10 +420,10 @@ private static MediaSource createYoutubeMediaSource(final Stream stream,
final PlayerDataSource dataSource,
final String cacheKey,
final MediaItemTag metadata)
- throws IOException {
+ throws ResolverException {
if (!(stream instanceof AudioStream || stream instanceof VideoStream)) {
- throw new IOException("Try to generate a DASH manifest of a YouTube "
- + stream.getClass() + " " + stream.getContent());
+ throw new ResolverException("Generation of YouTube DASH manifest for "
+ + stream.getClass().getSimpleName() + " is not supported");
}
final StreamType streamType = streamInfo.getStreamType();
@@ -430,15 +446,15 @@ private static MediaSource createYoutubeMediaSource(final Stream stream,
return buildYoutubeManualDashMediaSource(dataSource,
createDashManifest(manifestString, stream), stream, cacheKey,
metadata);
- } catch (final CreationException | NullPointerException e) {
+ } catch (final CreationException | IOException | NullPointerException e) {
Log.e(TAG, "Error when generating the DASH manifest of YouTube ended live stream",
e);
- throw new IOException("Error when generating the DASH manifest of YouTube ended "
- + "live stream " + stream.getContent(), e);
+ throw new ResolverException(
+ "Error when generating the DASH manifest of YouTube ended live stream", e);
}
} else {
- throw new IllegalArgumentException("DASH manifest generation of YouTube livestreams is "
- + "not supported");
+ throw new ResolverException(
+ "DASH manifest generation of YouTube livestreams is not supported");
}
}
@@ -447,7 +463,7 @@ private static MediaSource createYoutubeMediaSourceOfVideoStreamType(
final Stream stream,
final StreamInfo streamInfo,
final String cacheKey,
- final MediaItemTag metadata) throws IOException {
+ final MediaItemTag metadata) throws ResolverException {
final DeliveryMethod deliveryMethod = stream.getDeliveryMethod();
switch (deliveryMethod) {
case PROGRESSIVE_HTTP:
@@ -488,12 +504,11 @@ private static MediaSource createYoutubeMediaSourceOfVideoStreamType(
return buildYoutubeManualDashMediaSource(dataSource,
createDashManifest(manifestString, stream), stream, cacheKey,
metadata);
- } catch (final CreationException | NullPointerException e) {
+ } catch (final CreationException | IOException | NullPointerException e) {
Log.e(TAG,
"Error when generating the DASH manifest of YouTube OTF stream", e);
- throw new IOException(
- "Error when generating the DASH manifest of YouTube OTF stream "
- + stream.getContent(), e);
+ throw new ResolverException(
+ "Error when generating the DASH manifest of YouTube OTF stream", e);
}
case HLS:
return dataSource.getYoutubeHlsMediaSourceFactory().createMediaSource(
@@ -503,7 +518,7 @@ private static MediaSource createYoutubeMediaSourceOfVideoStreamType(
.setCustomCacheKey(cacheKey)
.build());
default:
- throw new IOException("Unsupported delivery method for YouTube contents: "
+ throw new ResolverException("Unsupported delivery method for YouTube contents: "
+ deliveryMethod);
}
}
@@ -535,4 +550,17 @@ private static ProgressiveMediaSource buildYoutubeProgressiveMediaSource(
.build());
}
//endregion
+
+
+ //region resolver exception
+ final class ResolverException extends Exception {
+ public ResolverException(final String message) {
+ super(message);
+ }
+
+ public ResolverException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+ }
+ //endregion
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java
index fd00d0ed9ab..6e18ee0cddd 100644
--- a/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java
+++ b/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java
@@ -23,7 +23,6 @@
import org.schabi.newpipe.player.mediaitem.StreamInfoTag;
import org.schabi.newpipe.util.ListHelper;
-import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@@ -94,8 +93,8 @@ public MediaSource resolve(@NonNull final StreamInfo info) {
final MediaSource streamSource = PlaybackResolver.buildMediaSource(
dataSource, video, info, PlaybackResolver.cacheKeyOf(info, video), tag);
mediaSources.add(streamSource);
- } catch (final IOException e) {
- Log.e(TAG, "Unable to create video source:", e);
+ } catch (final ResolverException e) {
+ Log.e(TAG, "Unable to create video source", e);
return null;
}
}
@@ -113,8 +112,8 @@ public MediaSource resolve(@NonNull final StreamInfo info) {
dataSource, audio, info, PlaybackResolver.cacheKeyOf(info, audio), tag);
mediaSources.add(audioSource);
streamSourceType = SourceType.VIDEO_WITH_SEPARATED_AUDIO;
- } catch (final IOException e) {
- Log.e(TAG, "Unable to create audio source:", e);
+ } catch (final ResolverException e) {
+ Log.e(TAG, "Unable to create audio source", e);
return null;
}
} else {
From e3c2aea3cc3abaaa86c36b0f8e8c6b6660a33795 Mon Sep 17 00:00:00 2001
From: AudricV <74829229+AudricV@users.noreply.github.com>
Date: Thu, 16 Jun 2022 11:15:05 +0200
Subject: [PATCH 078/240] Fix playback of non-URI HLS streams
A custom HlsPlaylistParserFactory cannot be used anymore to play HLS streams.
This needs to be replaced by a custom HlsDataSourceFactory, which returns a ByteArrayDataSource (where the bytes of this DataSource correspond to the bytes of the playlist string) and a specified DataSource for other request types.
This model has two limitations:
- if media requests are relative, the URI from which the manifest comes from (either the manifest URI (preferred) or the master URI (if applicable)) must be returned, otherwise the content will be not playable, as it will be an invalid URL, or it may be treat as something unexpected, for instance as a file for DefaultDataSources;
- if the playlist is a master playlist, endless loops should be encountered because the DataSources created for media playlists will use the master playlist response instead of fetching the corresponding playlist. With the current model of HlsDataSourceFactory, there is no possibility to distinguish the playlist type or the URI that is requested.
If ExoPlayer provides a way to create HlsMediaSources with an HlsPlaylist in the future, it should be used instead of this solution.
---
.../NonUriHlsDataSourceFactory.java | 136 ++++++++++++++++++
.../NonUriHlsPlaylistParserFactory.java | 50 -------
.../player/helper/PlayerDataSource.java | 13 +-
.../player/resolver/PlaybackResolver.java | 30 ++--
4 files changed, 153 insertions(+), 76 deletions(-)
create mode 100644 app/src/main/java/org/schabi/newpipe/player/datasource/NonUriHlsDataSourceFactory.java
delete mode 100644 app/src/main/java/org/schabi/newpipe/player/helper/NonUriHlsPlaylistParserFactory.java
diff --git a/app/src/main/java/org/schabi/newpipe/player/datasource/NonUriHlsDataSourceFactory.java b/app/src/main/java/org/schabi/newpipe/player/datasource/NonUriHlsDataSourceFactory.java
new file mode 100644
index 00000000000..676443a9c78
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/datasource/NonUriHlsDataSourceFactory.java
@@ -0,0 +1,136 @@
+package org.schabi.newpipe.player.datasource;
+
+import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
+
+import androidx.annotation.NonNull;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.source.hls.HlsDataSourceFactory;
+import com.google.android.exoplayer2.upstream.ByteArrayDataSource;
+import com.google.android.exoplayer2.upstream.DataSource;
+
+import java.nio.charset.StandardCharsets;
+
+/**
+ * A {@link HlsDataSourceFactory} which allows playback of non-URI media HLS playlists for
+ * {@link com.google.android.exoplayer2.source.hls.HlsMediaSource HlsMediaSource}s.
+ *
+ *
+ * If media requests are relative, the URI from which the manifest comes from (either the
+ * manifest URI (preferred) or the master URI (if applicable)) must be returned, otherwise the
+ * content will be not playable, as it will be an invalid URL, or it may be treat as something
+ * unexpected, for instance as a file for
+ * {@link com.google.android.exoplayer2.upstream.DefaultDataSource DefaultDataSource}s.
+ *
+ *
+ *
+ * See {@link #createDataSource(int)} for changes and implementation details.
+ *
+ */
+public final class NonUriHlsDataSourceFactory implements HlsDataSourceFactory {
+
+ /**
+ * Builder class of {@link NonUriHlsDataSourceFactory} instances.
+ */
+ public static final class Builder {
+ private DataSource.Factory dataSourceFactory;
+ private String playlistString;
+
+ /**
+ * Set the {@link DataSource.Factory} which will be used to create non manifest contents
+ * {@link DataSource}s.
+ *
+ * @param dataSourceFactoryForNonManifestContents the {@link DataSource.Factory} which will
+ * be used to create non manifest contents
+ * {@link DataSource}s, which cannot be null
+ */
+ public void setDataSourceFactory(
+ @NonNull final DataSource.Factory dataSourceFactoryForNonManifestContents) {
+ this.dataSourceFactory = dataSourceFactoryForNonManifestContents;
+ }
+
+ /**
+ * Set the HLS playlist which will be used for manifests requests.
+ *
+ * @param hlsPlaylistString the string which correspond to the response of the HLS
+ * manifest, which cannot be null or empty
+ */
+ public void setPlaylistString(@NonNull final String hlsPlaylistString) {
+ this.playlistString = hlsPlaylistString;
+ }
+
+ /**
+ * Create a new {@link NonUriHlsDataSourceFactory} with the given data source factory and
+ * the given HLS playlist.
+ *
+ * @return a {@link NonUriHlsDataSourceFactory}
+ * @throws IllegalArgumentException if the data source factory is null or if the HLS
+ * playlist string set is null or empty
+ */
+ @NonNull
+ public NonUriHlsDataSourceFactory build() {
+ if (dataSourceFactory == null) {
+ throw new IllegalArgumentException(
+ "No DataSource.Factory valid instance has been specified.");
+ }
+
+ if (isNullOrEmpty(playlistString)) {
+ throw new IllegalArgumentException("No HLS valid playlist has been specified.");
+ }
+
+ return new NonUriHlsDataSourceFactory(dataSourceFactory,
+ playlistString.getBytes(StandardCharsets.UTF_8));
+ }
+ }
+
+ private final DataSource.Factory dataSourceFactory;
+ private final byte[] playlistStringByteArray;
+
+ /**
+ * Create a {@link NonUriHlsDataSourceFactory} instance.
+ *
+ * @param dataSourceFactory the {@link DataSource.Factory} which will be used to build
+ * non manifests {@link DataSource}s, which must not be null
+ * @param playlistStringByteArray a byte array of the HLS playlist, which must not be null
+ */
+ private NonUriHlsDataSourceFactory(@NonNull final DataSource.Factory dataSourceFactory,
+ @NonNull final byte[] playlistStringByteArray) {
+ this.dataSourceFactory = dataSourceFactory;
+ this.playlistStringByteArray = playlistStringByteArray;
+ }
+
+ /**
+ * Create a {@link DataSource} for the given data type.
+ *
+ *
+ * Contrary to {@link com.google.android.exoplayer2.source.hls.DefaultHlsDataSourceFactory
+ * ExoPlayer's default implementation}, this implementation is not always using the
+ * {@link DataSource.Factory} passed to the
+ * {@link com.google.android.exoplayer2.source.hls.HlsMediaSource.Factory
+ * HlsMediaSource.Factory} constructor, only when it's not
+ * {@link C#DATA_TYPE_MANIFEST the manifest type}.
+ *
+ *
+ *
+ * This change allow playback of non-URI HLS contents, when the manifest is not a master
+ * manifest/playlist (otherwise, endless loops should be encountered because the
+ * {@link DataSource}s created for media playlists should use the master playlist response
+ * instead).
+ *
şərhlərdəki keçidləri tıklanabilir etmək, mətn ölçüsünü artırmaq
+
şərhlərdə vaxt damğası bağlantılarına klikləməyə çalışın
+
son seçilmiş vəziyyətə əsasən üstünlük verilən tabı göstərin
+
pleylist pəncərəsində "Arxa fon" üzərinə uzun müddət kliklədikdə pleylistini növbəyə əlavə edin
+
URL olmadığı zaman paylaşılan mətni axtarın
<
+li> əsas video pleyerə "cari vaxtda paylaş" düyməsini əlavə edin
+
video növbəsi bitdikdə əsas pleyerə bağlama düyməsini əlavə edin
+
video siyahı elementləri üçün uzun basaraq menyuya "Birbaşa Fonda Oynat" əlavə edin
Play/Enqueue əmrləri üçün ingilis dili tərcümələrini təkmilləşdirin
+
kiçik performans təkmilləşdirmələri
+
istifadə olunmamış faylları silin
+
ExoPlayer-i 2.9.6-a yeniləyin
+
Invidious bağlantılar üçün dəstək əlavə edin
+
+
Sabit
+
+
şərhlər və əlaqəli axınlarla sabit sürüşdürmə deaktiv edildi
+
Sabit CheckForNewAppVersionTask yerinə yetirilməməlidir
+
sabit youtube abunə idxalı: etibarsız url olanlara məhəl qoymayın və başlığı boş olanları saxlayın
+
etibarsız YouTube url-ni düzəldin: imza teqinin adı həmişə "imza" deyil, axınların yüklənməsinə mane olur
+
diff --git a/fastlane/metadata/android/az/changelogs/750.txt b/fastlane/metadata/android/az/changelogs/750.txt
new file mode 100644
index 00000000000..b00c911c065
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/750.txt
@@ -0,0 +1,21 @@
+Yeni
+Oxuma xülasəsi #2288
+• Sonuncu dəfə dayandırdığınız yayımları davam etdirin
+Yükləyici Təkmilləşdirmələri #2149
+ • Endirmələri xarici SD-kartlarda saxlamaq üçün Yaddaş Giriş Çərçivəsindən istifadə edin
+• Yeni mp4 muxer
+ • Yükləməyə başlamazdan əvvəl yükləmə qovluğunu istəyə görə dəyişdirin
+ • Ölçülmüş şəbəkələrə hörmət edin
+
+Təkmilləşdirilmiş
+ • Silinmiş gema sətirləri #2295
+ • Fəaliyyətin həyat dövrü ərzində (avtomatik) fırlanma dəyişikliklərini idarə edin #2444
+ • Uzun basılan menyuları ardıcıl edin #2368
+
+Düzəltildi
+• Seçilmiş altyazı trek adının göstərilməməsi düzəldildi #2394
+ • Proqram yeniləməsinin yoxlanılması uğursuz olduqda qəzaya uğramayın (GitHub versiya) #2423
+ • Sabit endirmələr 99,9% qalıb #2440
+ • Oynatma növbəsi metadatasını güncəlləyin #2453
+• [SoundCloud] Pleylistləri yükləyərkən yaranan xəta düzəldildi TeamNewPipe/NewPipeExtractor#170
+ • [YouTube] Sabit müddət Team7Pipe/Newtractor paresed edilə bilməz
diff --git a/fastlane/metadata/android/az/changelogs/760.txt b/fastlane/metadata/android/az/changelogs/760.txt
new file mode 100644
index 00000000000..dcb467f9979
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/760.txt
@@ -0,0 +1,36 @@
+0.17.1-də dəyişikliklər
+
+Yeni
+ • Tayland lokalizasiyası
+
+Təkmilləşdi
+• Pleylistlər üçün uzun basılan menyularda yenidən burada ifa etməyə başlayın əməliyyatı əlavə edin #2518
+• SAF / köhnə fayl seçicisi üçün keçid əlavə edin #2521
+
+ Sabitləşdirildi
+• Proqramları dəyişdirərkən yükləmələr görünüşündə yoxa çıxan düymələri düzəldin #2487
+• Baxış tarixçəsi deaktiv edilsə də, oxutma mövqeyini düzəldin
+• Siyahı görünüşlərində oxutma mövqeyinin yaratdığı performansı azaldın №2517
+• [Extractor] ReCaptchaActivity #2527, TeamNewPipe/NewPipeExtractor#186-nı düzəldin
+ • [Extractor] [YouTube] Təsadüfi axtarış xətasını düzəldin. çalğı siyahıları nəticələrdədir TeamNewPipe/NewPipeExtractor#185
+
+
+ 0.17.0-da dəyişikliklər
+ Yeni
+Oynatma davamı #2288
+ • Sonuncu dəfə dayandırdığınız yayımları davam etdirin Downloader Təkmilləşdirmələri #2149
+ • Endirmələri xarici SD-kartlarda saxlamaq üçün Yaddaş Giriş Çərçivəsindən istifadə edin
+• Seçim olaraq yeni mp4 muxer. endirməyə başlamazdan əvvəl endirmə kataloqunu dəyişdirin
+ • Ölçülmüş şəbəkələrə hörmət edin
+
+Təkmilləşdirildi
+• Silinmiş gema sətirləri #2295
+ • Fəaliyyətin həyat dövrü ərzində (avtomatik) fırlanma dəyişikliklərini idarə edin #2444
+ • Uzun müddətə edin -menyuları ardıcıl basın #2368
+
+ Sabitləndi
+ • Seçilmiş altyazı trekinin adının göstərilməməsi düzəldildi #2394
+• Proqram yeniləməsinin yoxlanılması uğursuz olduqda qəzaya uğramayın (GitHub versiyası) #2423
+ • Sabit endirmələr 99,9%-də qaldı #2440
+• Oynatma növbəsinin metadatasını yeniləyin #2453
+• [SoundCloud] TeamNewPipe/NewPipeExtractor#170 pleylistlərini yükləyərkən yaranan xəta düzəldildi • [YouTube] Sabit müddət TeamNewPipe/NewPipeExtractor#177 ilə paresd edilə bilməz
diff --git a/fastlane/metadata/android/az/changelogs/770.txt b/fastlane/metadata/android/az/changelogs/770.txt
new file mode 100644
index 00000000000..6ede739464f
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/770.txt
@@ -0,0 +1,4 @@
+0.17.2 Dəyişiklikləri
+
+Düzəlt
+• Düzəldi. Heç bir video mövcud deyildi
diff --git a/fastlane/metadata/android/az/changelogs/780.txt b/fastlane/metadata/android/az/changelogs/780.txt
new file mode 100644
index 00000000000..88d760a7c22
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/780.txt
@@ -0,0 +1,12 @@
+0.17.3-də dəyişikliklər
+
+ Təkmilləşdi
+• Oxutma vəziyyətlərini silmək üçün seçim əlavə edildi #2550
+• Fayl seçicidə gizli kataloqları göstərin #2591
+• NewPipe #2488 ilə açılacaq `invidio.us` instansiyalarından URL-ləri dəstəkləyin
+ • `music.youtube üçün dəstək əlavə edin .com` URL'ləri TeamNewPipe/NewPipeExtractor#194
+
+Düzəltildi
+• [YouTube] Sabit 'java.lang.IllegalArgumentException #192
+ • [YouTube] Sabit canlı yayımların işləməməsi TeamNewPipe/NewPipeExtractor#195-in işləməsi problemi
+ • #52-də endirmə düzəlib
diff --git a/fastlane/metadata/android/az/changelogs/790.txt b/fastlane/metadata/android/az/changelogs/790.txt
new file mode 100644
index 00000000000..7254327906e
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/790.txt
@@ -0,0 +1,14 @@
+Təkmilləşdirilmiş
+• Kor insanlar üçün əlçatanlığı yaxşılaşdırmaq üçün daha çox başlıq əlavə edin #2655
+• Yükləmə qovluğunun dilini daha ardıcıl və daha az qeyri-müəyyən edin #2637
+
+Düzəltildi
+• Blokdakı son baytın endirilib-endirilmədiyini yoxlayın #2646
+• Video təfərrüatlı fraqmentdə sabit sürüşdürmə #2672
+ • İkiqat axtarışın aydın qutusu animasiyalarını bir #2695-ə silin
+• [SoundCloud] Client_id hasilatı #2745
+
+İnkişafını düzəldin
+ • NewPipeExtractor-dan NewPipe #2535-ə miras qalmış çatışmayan asılılıqları əlavə edin
+• AndroidX #2685-ə köçürün
+ • ExoPlayer 2.10.6 #2736-a yeniləyin, #2
diff --git a/fastlane/metadata/android/az/changelogs/800.txt b/fastlane/metadata/android/az/changelogs/800.txt
new file mode 100644
index 00000000000..b6c00667e25
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/800.txt
@@ -0,0 +1,25 @@
+Yeni
+ • P2P olmadan PeerTube dəstəyi (#2201)
+[Beta]:
+◦ PeerTube nümunələrindən videolara baxın və endirin
+◦ Tam PeerTube dünyasına daxil olmaq üçün parametrlərə nümunələr əlavə edin
+ ◦ Müəyyən girişlərə daxil olan zaman Android 4.4 və 7.1-də SSL əl sıxmalarında problemlər ola bilər. şəbəkə xətası ilə nəticələnən hallar.
+
+ • Yükləyici (#2679):
+◦ Endirmə ETA-nı hesablayın
+◦ Opus (veb faylları) ogg kimi endirin
+ ◦ Uzun fasilədən sonra endirmələri davam etdirmək üçün vaxtı keçmiş endirmə bağlantılarını bərpa edin
+
+ Təkmilləşdirildi
+ • KioskFragment-i üstünlük verilən məzmun ölkəsindəki dəyişikliklərdən xəbərdar edin və bütün performansını yaxşılaşdırın əsas nişanlar #2742
+• №2713 çıxarıcıdan yeni Lokallaşdırma və Yükləyici tətbiqlərindən istifadə edin
+ • "Defolt köşk" sətrini tərcümə edilə bilən edin
+ • Qara mövzu üçün qara naviqasiya paneli #2569
+
+ Sabitləşdirildi
+• Başqa barmaq yerləşdirilərkən pop-up pleyerini hərəkət etdirə bilməyən xəta düzəldildi. pop-up pleyerinin hərəkət etdirilməsi #2772
+• Yükləyicisi çatmayan pleylistlərə icazə verin və bu problemlə bağlı qəzaları düzəldin №2724, TeamNewPipe/NewPipeExtractor#219
+ • MediaC ilə TLS əl sıxışmasını düzəltmək üçün Android 4.4 cihazlarında (API 19/KitKat) TLS1.1/1.2 işə salınır və bəzi PeerTube nümunələri #2792
+ • [SoundCloud] Sabit müştəri kimliyinin çıxarılması TeamNewPipe/NewPipeExtractor#217 • [SoundCloud] Audio axınının çıxarılmasının işlənməsini düzəldin
+• ExoPlayer-i 2.10.8 #2791, #2816-a yeniləyin
+• Güncəlləmə 3.5.1-ə keçin və Kotlin dəstəyi #2714 əlavə edin
diff --git a/fastlane/metadata/android/az/changelogs/810.txt b/fastlane/metadata/android/az/changelogs/810.txt
new file mode 100644
index 00000000000..5b99dec7f4c
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/810.txt
@@ -0,0 +1,18 @@
+Yeni
+• Arxa fonda ifa edərkən kilid ekranında video miniatürünü göstər
+
+Təkmilləşdirilmiş
+• Arxa fonda / popup düyməsini uzun müddət basdıqda növbəyə yerli pleylist əlavə edin
+ • Yalnız bir tab olduqda əsas səhifə nişanlarını sürüşdürün və gizlədin
+• Bildiriş miniatürünün miqdarını məhdudlaşdırın fon pleyerində yeniləmələr
+ • Boş yerli çalğı siyahıları üçün dummy miniatür əlavə edin
+• *.webm əvəzinə *.opus fayl uzantısından istifadə edin və endirmə açılan menyusunda "WebM Opus" əvəzinə format etiketində "opus"u göstərin • Yüklənmiş faylları silmək üçün düyməni əlavə edin və ya "Yükləmələr"də endirmə tarixçəsi
+ • [YouTube] /c/shortened_url kanal linklərinə dəstək əlavə edin
+
+Düzəltildi
+• Videonu NewPipe-da paylaşarkən və onun axınlarını birbaşa endirərkən bir çox problem həll edildi
+• Yaradılan mövzudan oyunçu girişi düzəldildi
+ • Sabit axtarış nəticəsi səhifələnməsi
+• [YouTube] NPE-yə səbəb olan null-un işə salınması düzəldildi
+• [YouTube] invidio.us url-i açarkən şərhlərə baxılması düzəldildi
+• [SoundCloud] Yenilənmiş müştəri_id
diff --git a/fastlane/metadata/android/az/changelogs/820.txt b/fastlane/metadata/android/az/changelogs/820.txt
new file mode 100644
index 00000000000..137109403fc
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/820.txt
@@ -0,0 +1 @@
+YouTube-u yararsız hala gətirən regex funksiyasının şifrəsinin açılması düzəldildi.
diff --git a/fastlane/metadata/android/az/changelogs/830.txt b/fastlane/metadata/android/az/changelogs/830.txt
new file mode 100644
index 00000000000..8c17dfc6318
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/830.txt
@@ -0,0 +1 @@
+SoundCloud problemlərini həll etmək üçün yenilənmiş SoundCloud client_id.
diff --git a/fastlane/metadata/android/az/changelogs/840.txt b/fastlane/metadata/android/az/changelogs/840.txt
new file mode 100644
index 00000000000..ff7cc23bf71
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/840.txt
@@ -0,0 +1,21 @@
+Yeni
+ • Proqram dilini dəyişmək üçün dil seçicisi əlavə edildi
+ • Oyunçuların yığıla bilən menyusuna Kodi-yə göndər düyməsi əlavə edildi
+ • Uzun basışda şərhləri kopyalamaq imkanı əlavə edildi
+
+Təkmilləşdirildi
+ • ReCaptcha fəaliyyətini düzəldin və əldə edilmiş kukiləri düzgün saxlayın
+ • Çekmece və gizlətmə lehinə nöqtə menyusu silindi parametrlərdə baxış tarixçəsi aktiv edilmədikdə tarix düyməsi
+• Android 6 və sonrakı versiyalarda düzgün şəkildə parametrlərdə digər proqramlar üzərində göstərilməsi üçün icazə istəyin
+ • BookmarkFragment-də uzun klikləməklə yerli pleylistinin adını dəyişin
+ • Müxtəlif PeerTube təkmilləşdirmələri
+• Təkmilləşdirilmiş bir neçə ingilis mənbə sətirləri
+
+Sabitləşdirildi
+• Sabit pleyer "Tətbiq keçidində minimuma endirmək" seçimi aktiv edildikdə və NewPipe minimuma endirildikdə dayandırılsa da, yenidən başlayır
+• Jest üçün ilkin parlaqlıq dəyərini düzəldin
+• Sabit .srt altyazı yükləmələri bütün sətir fasilələrini ehtiva etmədi
+• SD karta endirmənin uğursuzluğu müəyyən edildi, çünki bəzi Android 5 cihazları CTF uyğun deyil
+ • Android KitKat-da sabit endirmə • Sabit pozulmuş video .mp4 faylının audio fayl kimi tanınması
+ • Sabit multi yanlış Çin dili kodları da daxil olmaqla ple lokalizasiya problemləri
+• [YouTube] Təsvirdəki vaxt möhürləri yenidən klikləilə bilər
diff --git a/fastlane/metadata/android/az/changelogs/850.txt b/fastlane/metadata/android/az/changelogs/850.txt
new file mode 100644
index 00000000000..ac92be0fbfa
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/850.txt
@@ -0,0 +1 @@
+Bu buraxılışda YouTube veb-saytının versiyası yeniləndi. Köhnə vebsayt versiyası mart ayında dayandırılacaq və buna görə də sizdən yeniləməyiniz tələb olunur.
diff --git a/fastlane/metadata/android/az/changelogs/860.txt b/fastlane/metadata/android/az/changelogs/860.txt
new file mode 100644
index 00000000000..ebe86f2e46f
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/860.txt
@@ -0,0 +1,8 @@
+Təkmilləşdirilmiş
+• Səthin və tempin açılıb
+-açılmadığını yadda saxlayın və bərpa edin
+ • Pleyerdə displey kəsilməsini dəstəkləyin
+ • Dairəvi görünüş və abunəçilərin sayı
+• Daha az məlumat istifadə etmək üçün optimallaşdırılmış YouTube
+
+Bu buraxılışda YouTube ilə əlaqəli 15-dən çox baq düzəldildi.
diff --git a/fastlane/metadata/android/az/changelogs/870.txt b/fastlane/metadata/android/az/changelogs/870.txt
new file mode 100644
index 00000000000..20689e58a6c
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/870.txt
@@ -0,0 +1,2 @@
+Bu, SoundCloud-dan yenidən böyük çətinliklər olmadan istifadə etməyə imkan vermək üçün NewPipe-ı yeniləyən düzəliş buraxılışıdır.
+SoundCloud-un v2 API-si indi ekstraktorda istifadə olunur və etibarsız müştəri ID-lərinin aşkarlanması təkmilləşdirilib.
diff --git a/fastlane/metadata/android/az/changelogs/900.txt b/fastlane/metadata/android/az/changelogs/900.txt
new file mode 100644
index 00000000000..f55355ae5d8
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/900.txt
@@ -0,0 +1,14 @@
+Yeni
+• Abunə qrupları və çeşidlənmiş lentlər
+• Pleyerlərdə səssiz düyməsi
+
+Təkmilləşdirilmiş
+ • NewPipe-də music.youtube.com və media.ccc.de linklərinin açılmasına icazə verin
+ • Görünüşdən Məzmuna iki parametrin yerini dəyişin
+• Dəqiq olduqda 5, 15, 25 saniyəlik axtarış seçimlərini gizlədin. axtarış aktivləşdirildi
+
+Sabitləşdirildi
+ • bəzi WebM videoları axtarıla bilməz
+• Android P-də verilənlər bazası ehtiyat nüsxəsi
+• endirilmiş faylı paylaşarkən qəza
+• tonlarla YouTube çıxarılması problemi və s.
diff --git a/fastlane/metadata/android/az/changelogs/910.txt b/fastlane/metadata/android/az/changelogs/910.txt
new file mode 100644
index 00000000000..b2ce71347f1
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/910.txt
@@ -0,0 +1 @@
+Bəzi nadir hallarda NewPipe-ın başlamasına mane olan sabit məlumatlar bazası miqrasiyası.
diff --git a/fastlane/metadata/android/az/changelogs/920.txt b/fastlane/metadata/android/az/changelogs/920.txt
new file mode 100644
index 00000000000..50ed91445f1
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/920.txt
@@ -0,0 +1,9 @@
+Təkmilləşdi
+
+ • Yayım şəbəkəsi elementlərinə yükləmə tarixi və baxış sayı əlavə edildi
+• Çekmə başlığının tərtibatı üçün təkmilləşdirmələr
+
+ Sabitləndi
+
+• API 19-da qəzalara səbəb olan sabit səssiz düyməsi
+• Uzun 1080p 60 kadr videoların endirilməsi düzəldildi
diff --git a/fastlane/metadata/android/az/changelogs/930.txt b/fastlane/metadata/android/az/changelogs/930.txt
new file mode 100644
index 00000000000..e27870b5d8e
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/930.txt
@@ -0,0 +1,17 @@
+Yeni
+• YouTube Musiqidə axtarın
+• Əsas Android TV dəstəyi
+
+Təkmilləşdi
+ • Bütün baxılan videoları yerli pleylistdən silmək imkanı əlavə edildi
+• Məzmunu pozmaq əvəzinə hələ dəstəklənməyəndə mesajı göstərin
+ • Çimdik jestləri ilə təkmilləşdirilmiş pop-up pleyerinin ölçüsünü dəyişdirin
+• Yayımları sıralayın arxa fonda və kanalda açılan düymələrə uzun müddət basmaq
+ • Çekmece başlığının başlığının təkmilləşdirilmiş ölçü idarəsi
+
+Sabitləndi
+• Yaş məhdudiyyəti ilə bağlı sabit məzmun parametri işləmir
+ • Müəyyən növ reCAPTCHA-lar düzəldildi
+ • Pleylist `null` olarkən əlfəcinləri açarkən qəza düzəldildi
+ • Şəbəkənin sabit aşkarlanması əlaqədar istisnalar
+ • Abunəliklər fraqmentində qrup çeşidləmə düyməsinin sabit görünməsi və daha çox
diff --git a/fastlane/metadata/android/az/changelogs/940.txt b/fastlane/metadata/android/az/changelogs/940.txt
new file mode 100644
index 00000000000..383d9d1db8d
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/940.txt
@@ -0,0 +1,14 @@
+Yeni
+ • SoundCloud şərhləri üçün dəstək əlavə edin
+ • YouTube məhdudlaşdırılmış rejim ayarı əlavə edin
+• PeerTube ana kanalı təfərrüatlarını göstərin
+
+Təkmilləşdi
+• Yalnız dəstəklənən xidmətlər üçün Kore düyməsini göstərin
+ • Naviqasiya Panelində və ya StatusBar-da başlayan oyunçu jestlərini bloklayın
+• Xidmətə əsasən yenidən cəhd edin və abunə olun düymələrinin fon rəngini dəyişdirin rəng
+
+Düzəltildi
+ • Yükləmə dialoqunun dondurulmasını düzəldin
+ • Brauzerdə aç düyməsi indi həqiqətən brauzerdə açılır
+ • Videoların açılması zamanı yaranan nasazlığı aradan qaldırın və "Bu yayımı oynatmaq mümkün olmadı" və s.
diff --git a/fastlane/metadata/android/az/changelogs/950.txt b/fastlane/metadata/android/az/changelogs/950.txt
new file mode 100644
index 00000000000..ecd70b6ecdb
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/950.txt
@@ -0,0 +1,4 @@
+Bu buraxılış üç kiçik düzəliş gətirir:
+• Adroid 10+-da sabit yaddaş girişi
+• Sabit açılış köşkləri
+• Uzun videoların sabit müddət təhlili
diff --git a/fastlane/metadata/android/az/changelogs/951.txt b/fastlane/metadata/android/az/changelogs/951.txt
new file mode 100644
index 00000000000..1ad7cf79b7e
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/951.txt
@@ -0,0 +1,16 @@
+Yeni
+
+ • Lent qrupu dialoqunda abunə seçicisi üçün axtarış əlavə edin
+• Yalnız qruplaşdırılmamış abunələri göstərmək üçün lent qrupu dialoquna filtr əlavə edin
+• Əsas səhifəyə pleylist nişanı əlavə edin
+• Arxa fonda/pop-up oyunçu növbəsində sürətli irəli/geri sarın
+ •Axtarış təklifini göstərin: bunu nəzərdə tutursunuz və nəticəni göstərirsiniz
+
+Təkmilləşdirildi
+ • Tətbiq metadatasını dəyişdirilmiş fayllarda buraxın
+• Uğursuz axınları növbədən silməyin
+• Alətlər panelinin rənginə uyğunlaşdırmaq üçün status panelinin rəngini güncəlləyin
+
+Sabit
+• Üzən nöqtə kümülatif xətaların səbəb olduğu sabit audio/video sinxronizasiyası düzəldildi
+ • [PeerTube ] Silinmiş şərhləri idarə edin və s
diff --git a/fastlane/metadata/android/az/changelogs/952.txt b/fastlane/metadata/android/az/changelogs/952.txt
new file mode 100644
index 00000000000..7cca8134319
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/952.txt
@@ -0,0 +1,8 @@
+Təkmilləşdirilmiş
+ • Avtomatik oxutma bütün xidmətlər üçün əlçatandır (yalnız YouTube üçün əvəzinə)
+
+Sabit
+• YouTube-un yeni davamını dəstəkləyərək əlaqəli axınlar
+düzəldildi
+• Sabit yaş məhdudiyyəti olan YouTube videoları
+• [Android TV] Sabit uzanan fokus vurğulanması
diff --git a/fastlane/metadata/android/az/changelogs/953.txt b/fastlane/metadata/android/az/changelogs/953.txt
new file mode 100644
index 00000000000..5c2c014af76
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/953.txt
@@ -0,0 +1 @@
+YouTube-un deşifrə funksiyasının çıxarılmasını düzəldin.
diff --git a/fastlane/metadata/android/az/changelogs/954.txt b/fastlane/metadata/android/az/changelogs/954.txt
new file mode 100644
index 00000000000..9141ddda91a
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/954.txt
@@ -0,0 +1,7 @@
+• yeni proqram iş prosesi: ətraflı səhifəsində videoları oynatın, oyunçunu minimuma endirmək üçün aşağı sürüşdürün
+ • MediaStyle bildirişləri: bildirişlərdə fərdiləşdirilə bilən hərəkətlər, performans təkmilləşdirmələri
+ • NewPipe-dan masaüstü proqramı kimi istifadə edərkən əsas ölçülərin dəyişdirilməsi
+ • dəstəklənməyən URL tost zamanı açıq seçimlərlə dialoq göstərin
+• Uzaqdan olanları əldə etmək mümkün olmadıqda axtarış təklifi təcrübəsini təkmilləşdirin
+• Defolt video keyfiyyəti 720p60 (tətbiqdaxili pleyer) və 480p (pop-up pleyer) səviyyəsinə qaldırıldı
+• tonla səhv düzəltmələri və daha çox
diff --git a/fastlane/metadata/android/az/changelogs/955.txt b/fastlane/metadata/android/az/changelogs/955.txt
new file mode 100644
index 00000000000..8fd351f715c
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/955.txt
@@ -0,0 +1,3 @@
+[YouTube] Bəzi istifadəçilər üçün axtarışı düzəldin [YouTube] Təsadüfi deşifrə istisnalarını düzəldin
+
+ [SoundCloud] Çizgi ilə bitən URL-lər indi düzgün təhlil edildi
diff --git a/fastlane/metadata/android/az/changelogs/956.txt b/fastlane/metadata/android/az/changelogs/956.txt
new file mode 100644
index 00000000000..7a9f02b208b
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/956.txt
@@ -0,0 +1 @@
+[YouTube] Hər hansı videonu yükləyərkən yaranan nasazlıq aradan qaldırıldı
diff --git a/fastlane/metadata/android/az/changelogs/957.txt b/fastlane/metadata/android/az/changelogs/957.txt
new file mode 100644
index 00000000000..7f4363b172d
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/957.txt
@@ -0,0 +1,10 @@
+• Xüsusi növbə hərəkətlərini birinə birləşdirin
+• Oyunçu bağlamaq üçün iki barmaq jesti
+• reCAPTCHA kukilərinin təmizlənməsinə icazə verin
+• Bildirişi rəngləndirməmək üçün seçim
+• Sonsuz buferləşdirməni, NewPipe ilə paylaşarkən səhv davranışı və digər uyğunsuzluqları düzəltmək üçün video detallarının necə açıldığını təkmilləşdirin
+• YouTube videolarını sürətləndirin və yaş məhdudiyyəti olanları düzəldin
+• Sürətli irəli/geri çevirmə zamanı qəzanı düzəldin
+• Eskizləri dartmaqla siyahıları yenidən təşkil etməyin
+• Həmişə pop-up xassələrini xatırlayın
+• Santali dili əlavə edildi
diff --git a/fastlane/metadata/android/az/changelogs/958.txt b/fastlane/metadata/android/az/changelogs/958.txt
new file mode 100644
index 00000000000..0a133727dc5
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/958.txt
@@ -0,0 +1,15 @@
+Yeni və təkmilləşdirilmiş:
+ • Kilid ekranında miniatürü gizlətmək üçün seçim yenidən əlavə edildi
+• Lenti yeniləmək üçün çəkin
+• Yerli siyahıları əldə edərkən təkmilləşdirilmiş performans
+
+Sabit:
+ • RAM-dan çıxarıldıqdan sonra NewPipe-ı işə salarkən yaranan qəza düzəldildi
+ • İnternet bağlantısı olmadıqda işə salma zamanı yaranan qəza düzəldildi
+• Parlaqlıq və həcm jest parametrlərinə uyğun olaraq düzəldildi
+• [YouTube] Sabit uzun çalğı siyahıları
+
+Digər:
+ • Kodun təmizlənməsi və bir sıra daxili təkmilləşdirmələr
+• Asılılıq yeniləmələri
+• Tərcümə yeniləmələri.
diff --git a/fastlane/metadata/android/az/changelogs/959.txt b/fastlane/metadata/android/az/changelogs/959.txt
new file mode 100644
index 00000000000..8062c442e4d
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/959.txt
@@ -0,0 +1,3 @@
+Səhv reportyorunu açdıqdan sonra sonsuz qəza döngəsi düzəldildi.
+NewPipe tərəfindən avtomatik açıla bilən PeerTube nümunələrinin yenilənmiş siyahısı.
+Yenilənmiş tərcümələr.
diff --git a/fastlane/metadata/android/az/changelogs/960.txt b/fastlane/metadata/android/az/changelogs/960.txt
new file mode 100644
index 00000000000..549a0b409af
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/960.txt
@@ -0,0 +1,4 @@
+• Parametrlərdə ixrac verilənlər bazası seçiminin təkmilləşdirilmiş təsviri.
+• YouTube şərhlərinin təhlili düzəldildi.
+ • media.ccc.de xidmətinin sabit displey adı.
+ • Yenilənmiş tərcümələr.
diff --git a/fastlane/metadata/android/az/changelogs/961.txt b/fastlane/metadata/android/az/changelogs/961.txt
new file mode 100644
index 00000000000..1aba9dc5c47
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/961.txt
@@ -0,0 +1,11 @@
+• [YouTube] Mix dəstəyi
+ • [YouTube] İctimai yayımçılar və Covid-19 haqqında məlumatı göstərin
+• [media.ccc.de] Son videolar əlavə edildi
+• Somali tərcüməsi əlavə edildi
+
+• Çoxlu daxili təkmilləşdirmələr
+
+ • Pleyer daxilində videoların paylaşılması düzəldildi
+• Sabit boş ReCaptcha veb görünüşü • Siyahıdan axını silərkən baş verən qəza düzəldildi
+• [PeerTube] Sabit əlaqəli axınlar
+• [YouTube] Sabit YouTube Musiqi axtarışı
diff --git a/fastlane/metadata/android/az/changelogs/962.txt b/fastlane/metadata/android/az/changelogs/962.txt
new file mode 100644
index 00000000000..73d5db9d936
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/962.txt
@@ -0,0 +1,2 @@
+media.ccc.de xidmətinə "son" videolar əlavə edildi.
+Media.ccc.de xidmətinə canlı yayımlar və həmçinin canlı yayım dəstəyi əlavə edildi.
diff --git a/fastlane/metadata/android/az/changelogs/963.txt b/fastlane/metadata/android/az/changelogs/963.txt
new file mode 100644
index 00000000000..d45bbda0235
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/963.txt
@@ -0,0 +1 @@
+• [YouTube] Sabit kanalın davamı
diff --git a/fastlane/metadata/android/az/changelogs/964.txt b/fastlane/metadata/android/az/changelogs/964.txt
new file mode 100644
index 00000000000..09b7d14520c
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/964.txt
@@ -0,0 +1,8 @@
+• Oyunçu idarələrində fəsillər üçün əlavə dəstək
+ • [PeerTube] Sepia axtarışı əlavə edildi
+• Video təfərrüatları görünüşündə paylaşma düyməsi yenidən əlavə edildi və axın təsviri tab tərtibatına köçürüldü
+• Parlaqlıq jesti deaktiv edilibsə, parlaqlığın bərpasını deaktiv edin
+ • Videonu kodi-də oynamaq üçün siyahı elementi əlavə edildi
+• Bəzi cihazlarda heç bir defolt brauzer təyin edilmədikdə qəza düzəldildi və paylaşma dialoqlarını təkmilləşdirin
+ • Tam ekran pleyerində hardware sahəsi düyməsi ilə oynat/fasilə dəyişin
+ • [media.ccc.de] Müxtəlif düzəlişlər və təkmilləşdirmələr
diff --git a/fastlane/metadata/android/az/changelogs/965.txt b/fastlane/metadata/android/az/changelogs/965.txt
new file mode 100644
index 00000000000..6fff273efe5
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/965.txt
@@ -0,0 +1,5 @@
+Kanal qruplarını yenidən sıralayarkən baş verən qəza düzəldildi.
+Kanallardan və pleylistlərdən daha çox YouTube videosu əldə etmək həll edildi.
+YouTube şərhlərinin alınması düzəldildi.
+YouTube URL-lərində /watch/, /v/ və /w/ alt yolları üçün əlavə dəstək.
+SoundCloud müştəri identifikatorunun və coğrafi məhdudiyyətli məzmunun sabit çıxarılması. Şimali Kürd lokalizasiyası əlavə edildi.
diff --git a/fastlane/metadata/android/az/changelogs/966.txt b/fastlane/metadata/android/az/changelogs/966.txt
new file mode 100644
index 00000000000..d34123e8b24
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/966.txt
@@ -0,0 +1,14 @@
+Yeni:
+• Yeni xidmət əlavə edin: Bandcamp
+
+Təkmilləşdirilmiş:
+• Proqramın cihaz mövzusunu izləməsi üçün seçim əlavə edin
+• Təkmil xəta panelini göstərməklə bəzi qəzaların qarşısını alın
+ • Məzmunun niyə əlçatmaz olması haqqında daha çox məlumat göstərin
+• Avadanlıq boşluq düyməsi oynatma/pauza verir
+ • "Yükləmə başladı" tostunu göstərin
+
+Sabit:
+ • Arxa fonda oxuyarkən video təfərrüatlarında çox kiçik miniatürləri düzəldin
+ • Kiçilmiş oyunçuda boş başlığı düzəldin
+• Düzgün bərpa edilməmiş sonuncu ölçü dəyişdirmə rejimi düzəldi
diff --git a/fastlane/metadata/android/az/changelogs/967.txt b/fastlane/metadata/android/az/changelogs/967.txt
new file mode 100644
index 00000000000..0a873dca33f
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/967.txt
@@ -0,0 +1 @@
+YouTube-un AB-də düzgün işləməməsi düzəldildi. Buna NewPipe-dan RAZILIQ kukisi təyin etməyi tələb edən yeni kuki və məxfilik razılığı sistemi səbəb olub.
diff --git a/fastlane/metadata/android/az/changelogs/968.txt b/fastlane/metadata/android/az/changelogs/968.txt
new file mode 100644
index 00000000000..8968c0e3d29
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/968.txt
@@ -0,0 +1,6 @@
+Uzun basılan menyuya kanal təfərrüatları seçimi əlavə edildi.
+Pleylist interfeysindən Pleylist adını dəyişmək üçün funksionallıq əlavə edildi.
+Video buferlənərkən istifadəçiyə fasilə verməyə icazə verin.
+Ağ mövzunu cilaladı.
+Daha böyük şrift ölçüsündən istifadə edərkən üst-üstə düşən şriftlər düzəldildi.
+Formuler və Zephier cihazlarında heç bir video düzəldilməyib. Müxtəlif qəzaları aradan qaldırdı.
diff --git a/fastlane/metadata/android/az/changelogs/969.txt b/fastlane/metadata/android/az/changelogs/969.txt
new file mode 100644
index 00000000000..dbea995adec
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/969.txt
@@ -0,0 +1,8 @@
+• Xarici yaddaşda quraşdırmaya icazə verin
+ • [Bandcamp] Yayımda ilk üç şərhi göstərmək üçün əlavə dəstək
+• Yükləmə başlandıqda yalnız "download başladı" tostunu göstərin
+• Saxlanılan kuki olmadıqda reCaptcha kukisini təyin etməyin
+• [Oyunçu] Keş performansını təkmilləşdirin
+• [Oyunçu] Sabit oyunçunun avtomatik oynamaması
+ • Yükləmələri silərkən əvvəlki Snackbarları rədd edin
+ • Siyahıda olmayan obyekti silmək cəhdi düzəldildi
diff --git a/fastlane/metadata/android/az/changelogs/970.txt b/fastlane/metadata/android/az/changelogs/970.txt
new file mode 100644
index 00000000000..66deec05fd9
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/970.txt
@@ -0,0 +1,10 @@
+Yeni
+ • Təsvirin altında məzmun metadatasını (teqlər, kateqoriyalar, lisenziya, ...) göstərin
+• Uzaqdan (yerli olmayan) pleylistlərdə "Kanal təfərrüatlarını göstər" seçimi əlavə edildi.
+ • Uzun basılan menyuya "Brauzerdə aç" seçimi əlavə edildi
+
+Sabit
+• Video təfərrüatları səhifəsində sabit fırlanma qəzası
+• Pleyerdəki "Kodi ilə Oyna" düyməsi həmişə Kore quraşdırmağı təklif edir
+• Sabit və təkmilləşdirilmiş qəbulu idxal və ixrac yolları
+• [YouTube] Bəyənmə sayı düzəldildi Və daha çox
diff --git a/fastlane/metadata/android/az/changelogs/971.txt b/fastlane/metadata/android/az/changelogs/971.txt
new file mode 100644
index 00000000000..bd06f076072
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/971.txt
@@ -0,0 +1,3 @@
+Düzəltmə
+• Rebuferdən sonra oxutma üçün buferi artırın
+ • Pleyerdə oynatma növbəsi ikonasına klikləyərkən planşetlərdə və televizorlarda yaranan nasazlıq aradan qaldırıldı
diff --git a/fastlane/metadata/android/az/changelogs/972.txt b/fastlane/metadata/android/az/changelogs/972.txt
new file mode 100644
index 00000000000..98a5d3142d8
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/972.txt
@@ -0,0 +1,10 @@
+Yeni
+Təsvirdə vaxt ştamplarını və hashtagları tanıyın Əl ilə planşet rejimi parametri əlavə edildi Lentdə oynanılan elementləri gizlətmək imkanı əlavə edildi
+
+Təkmilləşdirilmiş
+Yaddaş Giriş Çərçivəsini düzgün şəkildə dəstəkləyin
+Əlçatan olmayan və dayandırılmış kanalların daha yaxşı idarə edilməsi
+ Android 10+ istifadəçiləri üçün Android paylaşım vərəqi indi məzmunun başlığını göstərir.
+
+Yenilənmiş
+ Invidious instansiyaları və boru bağlantılarını dəstəkləyir. Sabit [YouTube] Yaş məhdudiyyəti olan məzmun Seçim dialoqunu açarkən sızan pəncərə İstisnasının qarşısını alın
diff --git a/fastlane/metadata/android/az/changelogs/973.txt b/fastlane/metadata/android/az/changelogs/973.txt
new file mode 100644
index 00000000000..f68571c5e5d
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/973.txt
@@ -0,0 +1,4 @@
+Düzəltmə
+ • Bir cərgəyə neçə videonun yerləşə biləcəyi ilə bağlı səhv hesablamaya görə tor düzümündə kəsilmiş miniatürləri və başlıqları düzəldin
+• Paylaşım menyusundan açılsa, yükləmə dialoqunun heç nə etmədən yoxa çıxmasını düzəldin
+• Storage Access Framework fayl seçicisi kimi xarici fəaliyyətlərin açılması ilə bağlı kitabxananı yeniləyin
diff --git a/fastlane/metadata/android/az/changelogs/974.txt b/fastlane/metadata/android/az/changelogs/974.txt
new file mode 100644
index 00000000000..46bd7793f96
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/974.txt
@@ -0,0 +1,4 @@
+Düzəltmə
+• YouTube tənzimləməsinin səbəb olduğu buferləmə problemlərini həll edin
+• YouTube şərhlərinin çıxarılmasını və əlil şərhlərlə qəzaları düzəldin
+• YouTube musiqi axtarışını düzəldin • PeerTube canlı yayımlarını düzəldin
diff --git a/fastlane/metadata/android/az/changelogs/975.txt b/fastlane/metadata/android/az/changelogs/975.txt
new file mode 100644
index 00000000000..5397ea02c05
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/975.txt
@@ -0,0 +1,17 @@
+Yeni
+ • Axtararkən miniatür önizləməsini göstərin
+• Əlil şərhləri aşkar edin
+• Lent elementini baxılmış kimi qeyd etməyə icazə verin
+ • Şərh ürəklərini göstərin
+
+Təkmilləşdirilmiş
+ • Metadata və teqlərin tərtibatını təkmilləşdirin
+• UI komponentlərinə xidmət rəngini tətbiq edin
+
+Sabit
+• Mini pleyerdə miniatürü düzəldin
+ • Dublikat növbə elementlərində sonsuz buferləşdirməni düzəldin
+• Fırlanma və daha sürətli bağlanma kimi bəzi oyunçu düzəlişləri
+• Fonda yüklənmiş qalan ReCAPTCHA-nı düzəldin
+• Lenti təzələyərkən klikləri söndürün
+ • Bəzi yükləyici qəzalarını düzəldin
diff --git a/fastlane/metadata/android/az/changelogs/976.txt b/fastlane/metadata/android/az/changelogs/976.txt
new file mode 100644
index 00000000000..fdc40a6e605
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/976.txt
@@ -0,0 +1,9 @@
+• Tam ekranda oyunçunu birbaşa açmaq üçün seçim əlavə edildi
+ • Hansı növ axtarış təkliflərinin göstərilməsinə icazə verin
+ • Qaranlıq mövzu indi daha tünd + tünd ekran əlavə edildi
+ • Arzuolunmaz faylları bozlaşdırmaq üçün təkmilləşdirilmiş fayl seçicisi
+• YouTube abunəliklərinin idxalı düzəldildi
+ • Yayımı təkrar oxutmaq üçün təkrar oxutma düyməsinə yenidən toxunmaq lazımdır
+• Sabit bağlanan audio sessiya
+• [Android TV] DPad istifadə edərkən uzun axtarış çubuğu atlamaları düzəldildi.
+Əlavə dəyişiklikləri görmək üçün aşağıdakı Linklər sekmesinden dəyişiklik jurnalına (və blog yazısına) baxın.
diff --git a/fastlane/metadata/android/az/changelogs/977.txt b/fastlane/metadata/android/az/changelogs/977.txt
new file mode 100644
index 00000000000..2d36e384fa9
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/977.txt
@@ -0,0 +1,8 @@
+• Uzun mətbuat menyusuna "növbəti oynat" düyməsi əlavə edildi
+• Niyyət filtrinə YouTube şortu yolu prefiksi əlavə edildi
+• Sabit Parametrlərin idxalı
+ • Növbə ekranında oyunçu düymələri ilə axtarış çubuğunun yerini dəyişdirin
+• MediasessionManager ilə bağlı müxtəlif düzəlişlər
+• Sabit axtarış çubuğu video bitdikdən sonra tamamlanmadı
+• RealtekATV-də media tunelləri söndürüldü
+• Genişləndirilmiş kiçildilmiş oyunçu düymələri tıklanabilir sahə Əlavə dəyişiklikləri görmək üçün aşağıdakı Linklər sekmesinden dəyişiklik jurnalına (və blog yazısına) baxın.
diff --git a/fastlane/metadata/android/az/changelogs/978.txt b/fastlane/metadata/android/az/changelogs/978.txt
new file mode 100644
index 00000000000..0c3ff16e133
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/978.txt
@@ -0,0 +1 @@
+Yeni bir NewPipe versiyası üçün yoxlamanın aparılması düzəldildi. Bu yoxlama bəzən çox erkən həyata keçirilirdi və buna görə də tətbiq qəzasına səbəb olur. Bu, indi düzəldilməlidir.
diff --git a/fastlane/metadata/android/az/changelogs/979.txt b/fastlane/metadata/android/az/changelogs/979.txt
new file mode 100644
index 00000000000..a74f76c708d
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/979.txt
@@ -0,0 +1,2 @@
+- Oynatmanın davam etdirilməsi düzəldildi
+- NewPipe-ın yeni versiyanın yoxlanılmasının olub olmadığını müəyyən edən xidmətin arxa planda başlamamasını təmin etmək üçün təkmilləşdirmələr
diff --git a/fastlane/metadata/android/az/changelogs/980.txt b/fastlane/metadata/android/az/changelogs/980.txt
new file mode 100644
index 00000000000..820abaaf387
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/980.txt
@@ -0,0 +1,12 @@
+Yeni
+ • Menyu paylaşmaq üçün "Pleylistə əlavə et" seçimi əlavə edildi
+ • y2u.be və PeerTube qısa keçidləri üçün əlavə dəstək
+
+ Təkmilləşdirilmiş • Playback-Speed-Controlları daha yığcam etdi
+• Lent indi yeni elementləri vurğulayır
+• Lentdə "Baxılan elementləri göstər" seçimi indi yadda saxlanılıb
+
+Sabit
+• Sabit YouTube bəyənmə və bəyənməmələrin çıxarılması
+• Arxa fondan qayıtdıqdan sonra avtomatik təkrar oynatma düzəldildi
+Və daha çox
diff --git a/fastlane/metadata/android/az/changelogs/981.txt b/fastlane/metadata/android/az/changelogs/981.txt
new file mode 100644
index 00000000000..f7fd2dec627
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/981.txt
@@ -0,0 +1,2 @@
+Android 11+-da buferdən sonra uğursuz oxutma davamını düzəltmək üçün MediaParser dəstəyi silindi.
+ Oxutma problemlərini həll etmək üçün Philips QM16XE-də media tunelini deaktiv etdi.
diff --git a/fastlane/metadata/android/az/changelogs/982.txt b/fastlane/metadata/android/az/changelogs/982.txt
new file mode 100644
index 00000000000..36b3e4d3256
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/982.txt
@@ -0,0 +1 @@
+YouTube-un heç bir yayım oynatmaması düzəldildi.
diff --git a/fastlane/metadata/android/az/changelogs/983.txt b/fastlane/metadata/android/az/changelogs/983.txt
new file mode 100644
index 00000000000..80c598fc270
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/983.txt
@@ -0,0 +1,6 @@
+Axtarmaq üçün yeni iki dəfə toxunan UI və davranış əlavə edin Parametrləri axtara bilən edin
+Sabitlənmiş şərhləri belə vurğulayın FSFE-nin PeerTube nümunəsi üçün açıq proqram dəstəyi əlavə edin Səhv bildirişləri əlavə edin
+ Oyunçu dəyişikliyində birinci növbə elementinin təkrarını düzəldin
+ Canlı yayımlar zamanı buferləmə zamanı uğursuzluqdan əvvəl daha çox gözləyin
+ Yerli axtarış nəticələrinin sırasını düzəldin
+Oyun növbəsindəki boş element sahələrini düzəldin
diff --git a/fastlane/metadata/android/az/changelogs/984.txt b/fastlane/metadata/android/az/changelogs/984.txt
new file mode 100644
index 00000000000..fdacd91aa42
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/984.txt
@@ -0,0 +1,6 @@
+Bütün ekranı doldurmaq və planşet və televizorlarda sürüşməni düzəltmək üçün siyahılara kifayət qədər ilkin elementləri yükləyin Siyahılar arasında sürüşərkən təsadüfi qəzaları düzəldin
+Oyunçuda sürətli axtarış üst-üstə düşmə qövsünü sistem UI-nin altına daxil edin
+ Çox pəncərədə oynadarkən bəzi telefonlarda yersiz oyunçu reqressiyasına səbəb olan dəyişiklikləri kəsiklərə qaytarın
+Sdk-ni 30-dan 31-ə qədər artırın
+Xəta hesabatı kitabxanasını yeniləyin
+Pleyerdə bəzi kodu refaktor edin
diff --git a/fastlane/metadata/android/az/changelogs/985.txt b/fastlane/metadata/android/az/changelogs/985.txt
new file mode 100644
index 00000000000..16a2e101364
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/985.txt
@@ -0,0 +1 @@
+YouTube-un heç bir yayım oynatmaması düzəldildi
diff --git a/fastlane/metadata/android/az/changelogs/986.txt b/fastlane/metadata/android/az/changelogs/986.txt
new file mode 100644
index 00000000000..a597de6afbb
--- /dev/null
+++ b/fastlane/metadata/android/az/changelogs/986.txt
@@ -0,0 +1,17 @@
+Yeni
+• Yeni axınlar üçün bildirişlər
+• Fon və video pleyerlər arasında problemsiz keçid
+• Səthi yarımtonlarla dəyişdirin
+• Pleylistə əsas oyunçu növbəsini əlavə edin
+
+ Təkmilləşdirilmiş
+• Sürət/pitch addım ölçüsünü yadda saxla
+ • Video pleyerdə ilkin uzun buferləməni azaldın
+ • Android TV üçün oyunçu interfeysini təkmilləşdirin
+• Bütün endirilmiş faylları silməzdən əvvəl təsdiqləyin
+
+Sabit
+• Yeni axınlar üçün bildirişlər
+• Fon və video pleyerlər arasında problemsiz keçid
+• Səthi yarımtonlarla dəyişdirin
+ • Pleylistə əsas oyunçu növbəsini əlavə edin
diff --git a/fastlane/metadata/android/az/full_description.txt b/fastlane/metadata/android/az/full_description.txt
index e8ba9d175e5..2843f8abeee 100644
--- a/fastlane/metadata/android/az/full_description.txt
+++ b/fastlane/metadata/android/az/full_description.txt
@@ -1 +1 @@
-Newpipe hər hansı Google çərçivə kitabxanası və ya Youtube API-si istifadə etmir. O sadəcə zəruri məlumatları toplamaq məqsədilə veb-saytı təhlil edir. Buna görə də bu tətbiqetmə Google Xidmətləri quraşdırılmamış cihazlarda istifadə edilə bilər. Həmçinin NewPipe istifadə etməyiniz üçün YouTube hesabına ehtiyacınız yoxdur və o, azad və açıq qaynaqlı proqramdır.
+Newpipe hər hansı Google çərçivə kitabxanası və ya Youtube API-si istifadə etmir. O sadəcə zəruri məlumatları toplamaq məqsədilə veb-saytı təhlil edir. Buna görə də bu tətbiqetmə Google Xidmətləri quraşdırılmamış cihazlarda istifadə edilə bilər. Həmçinin NewPipe istifadə etməyiniz üçün YouTube hesabına ehtiyacınız yoxdur və o, azad və açıq qaynaqlı tətbiqdir.
diff --git a/fastlane/metadata/android/be/short_description.txt b/fastlane/metadata/android/be/short_description.txt
new file mode 100644
index 00000000000..d689c171322
--- /dev/null
+++ b/fastlane/metadata/android/be/short_description.txt
@@ -0,0 +1 @@
+Свабодны і лёгкі кліент Youtube для Android.
diff --git a/fastlane/metadata/android/da/changelogs/63.txt b/fastlane/metadata/android/da/changelogs/63.txt
new file mode 100644
index 00000000000..6667e924e3a
--- /dev/null
+++ b/fastlane/metadata/android/da/changelogs/63.txt
@@ -0,0 +1,8 @@
+### Forbedringer
+- Import/export indstillinger #1333
+- Reducering af overtegning (ydeevne forbedring )#1371
+- Små kode forbedringer #1375
+- GDPR er nu dokumenteret #1420
+
+### Fikset
+- Downloader: Et crash når man loadede et ikke-færdiggjort download fra .giga filer #1407
diff --git a/fastlane/metadata/android/da/full_description.txt b/fastlane/metadata/android/da/full_description.txt
new file mode 100644
index 00000000000..db3d010a80b
--- /dev/null
+++ b/fastlane/metadata/android/da/full_description.txt
@@ -0,0 +1 @@
+NewPipe bruger ingen af Googles programmeringsplatforme eller YouTubes API. Det besøger kun hjemmesiden for at finde de informationer det har brug for. derfor kan denne app køre på enheder uden Google Services, desuden har du ikke brug for en Google konto for at bruge appen. NewPipe er udgivet under en fri og open source licens.
diff --git a/fastlane/metadata/android/da/short_description.txt b/fastlane/metadata/android/da/short_description.txt
new file mode 100644
index 00000000000..6d9b80afdea
--- /dev/null
+++ b/fastlane/metadata/android/da/short_description.txt
@@ -0,0 +1 @@
+En gratis let klient til YouTube på Android.
diff --git a/fastlane/metadata/android/eu/changelogs/65.txt b/fastlane/metadata/android/eu/changelogs/65.txt
new file mode 100644
index 00000000000..4d34a8ed147
--- /dev/null
+++ b/fastlane/metadata/android/eu/changelogs/65.txt
@@ -0,0 +1,14 @@
+### Hobekuntzak
+
+- Burger menuko animazioa desgaitu #1486
+- Deskargen ezabaketa desegin #1472
+- Partekatze-menuan deskargatzeko aukera #1498
+- Sakatze luzeko menuan partekatzeko aukera gehitu da #1454
+- Minimizatu erreproduzigailua irtetzean #1354
+- Liburutegiaren bertsioa eguneratu eta datu-basearen babeskopia konpondu #1510
+- ExoPlayer 2.8.2 eguneraketa #1392
+
+### Konponketak
+
+- #1440 Bideoaren informazio ikuspegia apurtuta konponduta #1491
+- Historia ikustea konponduta #1497
diff --git a/fastlane/metadata/android/fil/changelogs/63.txt b/fastlane/metadata/android/fil/changelogs/63.txt
new file mode 100644
index 00000000000..83f5e586984
--- /dev/null
+++ b/fastlane/metadata/android/fil/changelogs/63.txt
@@ -0,0 +1,8 @@
+### Mga pagpapabuti
+- Pag-import/export ng mga setting #1333
+- Bawas na overdraw (nakakabuti sa performance) #1371
+- Mga pagpapaganda sa code #1375
+- Mga bagay-bagay tungkol sa GDPR #1420
+
+### Inayos
+- Downloader: Ayusin ang pag-crash tuwing binubuksan ang mga .giga file na hindi pa tapos i-download #1407
diff --git a/fastlane/metadata/android/fil/changelogs/64.txt b/fastlane/metadata/android/fil/changelogs/64.txt
new file mode 100644
index 00000000000..7b7227837c5
--- /dev/null
+++ b/fastlane/metadata/android/fil/changelogs/64.txt
@@ -0,0 +1,8 @@
+### Mga pagpapabuti
+- Maaari nang limitahan ang kalidad ng video kung gumagamit ng mobile data. #1339
+- Tandaan ang liwanag ng screen para sa sesyon #1442
+- Mas pinainam na pag-download sa mahihinang CPU #1431
+- (Gumaganang) suporta para sa media session #1433
+
+### Fix
+- Inayos ang crash tuwing binubuksan ang mga download (nasa release builds na rin ito) #1441
diff --git a/fastlane/metadata/android/fil/full_description.txt b/fastlane/metadata/android/fil/full_description.txt
index b910a60a1f7..1c8fe221a5c 100644
--- a/fastlane/metadata/android/fil/full_description.txt
+++ b/fastlane/metadata/android/fil/full_description.txt
@@ -1 +1 @@
-Hindi gumagamit ang NewPipe ng kahit anong Google framework libraries o ang YouTube API. Pinaparse niya lang ang website upang makakalap ng impormasyong kinakailangan nito. Samakatuwid ang app na ito ay puwedeng gamitin sa mga device na walang naka install na Google Services. Hindi mo rin kailangan ng YouTube account upang magamit ang NewPipe at saka ito ay FLOSS.
+Hindi gumagamit ng mga framework library ng Google o API ng YouTube ang NewPipe. Pina-parse lang nito ang website upang makuha ang kinakailangang impormasyon, kaya maaaring itong gamitin sa mga device kung saan hindi naka-install ang Google Services. FLOSS ito, at hindi mo rin kailangang gumamit ng account.
diff --git a/fastlane/metadata/android/fil/short_description.txt b/fastlane/metadata/android/fil/short_description.txt
index 6e3aa8fa840..839d71d57e9 100644
--- a/fastlane/metadata/android/fil/short_description.txt
+++ b/fastlane/metadata/android/fil/short_description.txt
@@ -1 +1 @@
-Isang magaan at libreng YouTube para sa Android.
+Libre't magaang YouTube frontend para sa Android.
diff --git a/fastlane/metadata/android/fr/changelogs/986.txt b/fastlane/metadata/android/fr/changelogs/986.txt
new file mode 100644
index 00000000000..b5f2af82130
--- /dev/null
+++ b/fastlane/metadata/android/fr/changelogs/986.txt
@@ -0,0 +1,13 @@
+Ajouts
+• Notifications pour les nouveaux flux
+• Transition fluide entre les lecteurs vidéo et en arrière-plan
+• Modification de la hauteur de son par demi-tons
+• Ajout de la file du lecteur principal dans une liste de lecture
+
+Améliorations
+• Enregistrement du pas de la vitesse et de la hauteur audios
+• Réduction du chargement initial d’une vidéo
+• Confirmer avant la suppression de tous les fichiers téléchargés
+
+Corrections
+• Réinitialisation de la lecture lors du changement de lecteur
diff --git a/fastlane/metadata/android/fr/short_description.txt b/fastlane/metadata/android/fr/short_description.txt
index e830dcf923c..a593ce32c93 100644
--- a/fastlane/metadata/android/fr/short_description.txt
+++ b/fastlane/metadata/android/fr/short_description.txt
@@ -1 +1 @@
-Lecteur multimédia libre et léger pour Android.
+Un lecteur multimédia libre et léger pour Android.
diff --git a/fastlane/metadata/android/it/changelogs/985.txt b/fastlane/metadata/android/it/changelogs/985.txt
index 296db3632dd..951ffee5ee6 100644
--- a/fastlane/metadata/android/it/changelogs/985.txt
+++ b/fastlane/metadata/android/it/changelogs/985.txt
@@ -1 +1 @@
-Sistemato un problema nell'estrattore di YouTube che impediva di guardare qualsiasi video.
\ No newline at end of file
+Corretto problema di riproduzione di YouTube
diff --git a/fastlane/metadata/android/lv/changelogs/63.txt b/fastlane/metadata/android/lv/changelogs/63.txt
new file mode 100644
index 00000000000..1d2f9d38f94
--- /dev/null
+++ b/fastlane/metadata/android/lv/changelogs/63.txt
@@ -0,0 +1,8 @@
+### Uzlabojumi
+- Importēšanas/eksportēšanas iestatījumi #1333
+- Samazināt pārzīmēšanu (ātruma uzlabojums) #1371
+- Nelieli koda uzlabojumi #1375
+- Pievienot visu par GDPR #1420
+
+### Salabots
+- Lejupielādētājs: Salabot avāriju, ielādējot nepabeigtas lejupielādes no .giga failiem #1407
diff --git a/fastlane/metadata/android/lv/changelogs/64.txt b/fastlane/metadata/android/lv/changelogs/64.txt
new file mode 100644
index 00000000000..b8d58e4c8e8
--- /dev/null
+++ b/fastlane/metadata/android/lv/changelogs/64.txt
@@ -0,0 +1,8 @@
+### Uzlabojumi
+- Pievienota iespēja ierobežot video kvalitāti, ja tiek lietoti mobilie dati. #1339
+- Atcerēties spilgtumu visu sesiju #1442
+- Uzlabot lejupielāžu ātrumu vājākiem procesoriem #1431
+-
+
+### Salabots
+- Salabot avāriju, kas notiek, kad atver lejupielādes () #1441
diff --git a/fastlane/metadata/android/lv/full_description.txt b/fastlane/metadata/android/lv/full_description.txt
index 4e9e29670ad..092644ed3a1 100644
--- a/fastlane/metadata/android/lv/full_description.txt
+++ b/fastlane/metadata/android/lv/full_description.txt
@@ -1 +1 @@
-NewPipe neizmanto nekādas Google bibliotēkas vai YouTube API. Tā tikai apstrāda vietni lai iegūtu nepieciešamo informāciju. Tāpēc šo lietotni var izmantot ierīces kurām nav Google pakalpojumi uzstadīti. Arī nav nepieciešams YouTube konts lai izmantotu NewPipe, un tas ir FLOSS.
+NewPipe neizmanto nekādas Google bibliotēkas vai YouTube API. Tā tikai apstrādā vietni, lai iegūtu nepieciešamo informāciju. Tāpēc šo lietotni var izmantot arī ierīcēs, kurās Google pakalpojumi nav uzstādīti. Nav pat nepieciešams YouTube konts, lai izmantotu NewPipe, un tas ir FLOSS.
diff --git a/fastlane/metadata/android/nl/changelogs/63.txt b/fastlane/metadata/android/nl/changelogs/63.txt
index 3a8c7df1f5c..ca6de697f63 100644
--- a/fastlane/metadata/android/nl/changelogs/63.txt
+++ b/fastlane/metadata/android/nl/changelogs/63.txt
@@ -5,4 +5,4 @@
- Alles toevoegen over GDPR #1420
### Opgelost
-- Downloader: Crash bij laden van onafgemaakte downloads van .giga bestanden verhelpen #1407
+- Downloader: Een crash verholpen bij laden van onafgemaakte downloads van .giga bestanden #1407
diff --git a/fastlane/metadata/android/nl/changelogs/985.txt b/fastlane/metadata/android/nl/changelogs/985.txt
index deaa12d0ca6..9bd8adf8693 100644
--- a/fastlane/metadata/android/nl/changelogs/985.txt
+++ b/fastlane/metadata/android/nl/changelogs/985.txt
@@ -1 +1 @@
-Opgelost: YouTube speelt geen stream af.
+YouTube speelt geen stream af opgelost
diff --git a/fastlane/metadata/android/nl/full_description.txt b/fastlane/metadata/android/nl/full_description.txt
index 3b8cef24420..b00bc774fd5 100644
--- a/fastlane/metadata/android/nl/full_description.txt
+++ b/fastlane/metadata/android/nl/full_description.txt
@@ -1 +1 @@
-NewPipe gebruikt geen enkele Google framework library, noch de YouTube API. Het analyseert de website alleen op zoek naar informatie die het nodig heeft. Daardoor kan deze app gebruikt worden zonder Google Services installatie. Daarnaast heb je geen YouTube-account nodig om NewPipe te gebruiken, en is het FLOSS.
+NewPipe gebruikt geen enkele Google framework bibliotheek, noch de YouTube API. Het analyseert de website op zoek naar enkel de informatie die het nodig heeft. Daardoor kan deze app gebruikt worden zonder dat Google Services is geïnstalleerd. Daarnaast heb je geen YouTube-account nodig om NewPipe te gebruiken, en is het FLOSS.
diff --git a/fastlane/metadata/android/tr/changelogs/65.txt b/fastlane/metadata/android/tr/changelogs/65.txt
new file mode 100644
index 00000000000..564cd60dc44
--- /dev/null
+++ b/fastlane/metadata/android/tr/changelogs/65.txt
@@ -0,0 +1,27 @@
+### İyileştirmeler
+
+- Burgermenu simge animasyonunu devre dışı bırak #1486
+
+- İndirilenlerin silinmesini geri al #1472
+- Paylaşım menüsünde indirme seçeneği #1498
+- Uzun dokunma menüsüne paylaşım seçeneği eklendi #1454
+- Çıkışta ana oynatıcıyı simge durumuna küçült #1354
+- Kitaplık sürümü güncellemesi ve veritabanı yedekleme düzeltmesi #1510
+- ExoPlayer 2.8.2 Güncelleme #1392
+- Daha hızlı hız değişimi için farklı adım boyutlarını desteklemek için oynatma hızı kontrol iletişim kutusu yeniden düzenlendi.
+- Oynatma hızı kontrolünde sessizlikler sırasında hızlı ileri sarmak için bir geçiş eklendi. Bu, sesli kitaplar ve belirli müzik türleri için faydalı olmalı ve gerçek bir kusursuz deneyim sunabilir (ve bir şarkıyı çok sayıda sessizlikle bozabilir =\\).
+- El ile yapmak yerine oynatıcıda dahili olarak medyanın yanında meta verilerin iletilmesine izin vermek için yeniden düzenlenmiş medya kaynağı çözünürlüğü. Artık tek bir meta veri kaynağımız var ve oynatma başladığında doğrudan kullanılabilir.
+- Oynatma listesi parçası açıldığında yeni meta veriler mevcut olduğunda güncellenmeyen uzak oynatma listesi meta verileri düzeltildi.
+- Çeşitli UI düzeltmeleri: #1383, arka plan oynatıcı bildirim kontrolleri artık her zaman beyaz, fırlatma yoluyla açılır pencere oynatıcısını kapatması daha kolay
+- Çoklu hizmet için yeniden düzenlenmiş mimariye sahip yeni çıkarıcı kullanın
+
+### Düzeltmeler
+
+- Düzeltme #1440 Kırık Video Bilgi Düzeni #1491
+- Geçmiş düzeltmesini görüntüle #1497
+- #1495, kullanıcı oynatma listesine erişir erişmez meta verileri (küçük resim, başlık ve video sayısı) güncelleyerek.
+- #1475, kullanıcı detay parçası üzerinde harici oynatıcıda bir video başlattığında veritabanına bir görünüm kaydederek.
+- Açılır pencere modunda ekran zaman aşımını düzeltin. #1463 (Sabit #640)
+- Ana video oynatıcı düzeltmesi #1509
+- [#1412] Oyuncu etkinliği arka plandayken yeni amaç alındığında oyuncunun NPE'sine neden olan sabit tekrar modu.
+- Oyuncunun açılır pencereye küçültülmesinin, açılır pencere izni verilmediğinde oyuncuyu yok etmemesi düzeltildi.
diff --git a/fastlane/metadata/android/tr/changelogs/66.txt b/fastlane/metadata/android/tr/changelogs/66.txt
new file mode 100644
index 00000000000..7d201f68531
--- /dev/null
+++ b/fastlane/metadata/android/tr/changelogs/66.txt
@@ -0,0 +1,31 @@
+# v0.13.7 değişiklik günlüğü
+### Düzeltildi
+- v0.13.6'nın sıralama filtresi sorunlarını düzeltin
+
+# v0.13.6'nın değişiklik günlüğü
+
+### İyileştirmeler
+
+- Burgermenu simge animasyonunu devre dışı bırak #1486
+- İndirilenlerin silinmesini geri al #1472
+- Paylaşım menüsünde indirme seçeneği # 1498
+- Uzun dokunma menüsüne paylaşım seçeneği eklendi #1454
+- Çıkışta ana oynatıcıyı simge durumuna küçült #1354
+- Kitaplık sürümü güncellemesi ve veritabanı yedekleme düzeltmesi #1510
+- ExoPlayer 2.8.2 Güncelleme #1392
+- Oynatma hızı kontrol iletişim kutusu, farklı adım boyutlarını desteklemek için yeniden çalışıldı daha hızlı hız değişimi.
+- Oynatma hızı kontrolünde sessizlikler sırasında hızlı ileri sarmak için bir geçiş eklendi. Bu, sesli kitaplar ve belirli müzik türleri için faydalı olmalı ve gerçek bir kusursuz deneyim sunabilir (ve bir şarkıyı çok sayıda sessizlikle bozabilir =\\).
+- El ile yapmak yerine oynatıcıda dahili olarak medyanın yanında meta verilerin iletilmesine izin vermek için yeniden düzenlenmiş medya kaynağı çözünürlüğü. Artık tek bir meta veri kaynağımız var ve oynatma başladığında doğrudan kullanılabilir.
+- Oynatma listesi parçası açıldığında yeni meta veriler mevcut olduğunda güncellenmeyen uzak oynatma listesi meta verileri düzeltildi.
+- Çeşitli UI düzeltmeleri: #1383, arka plan oynatıcı bildirim kontrolleri artık her zaman beyaz, fırlatma yoluyla açılır pencere oynatıcısını kapatması daha kolay
+- Çoklu hizmet için yeniden düzenlenmiş mimariye sahip yeni çıkarıcı kullanın
+
+### Düzeltmeler
+- Düzeltme #1440 Kırık Video Bilgi Düzeni #1491
+- Geçmiş düzeltmesini görüntüle #1497
+- #1495, kullanıcı oynatma listesine erişir erişmez meta verileri (küçük resim, başlık ve video sayısı) güncelleyerek.
+- #1475, kullanıcı detay parçası üzerinde harici oynatıcıda bir video başlattığında veritabanına bir görünüm kaydederek.
+- Açılır pencere modunda ekran zaman aşımını düzeltin. #1463 (Sabit #640)
+- Ana video oynatıcı düzeltmesi #1509
+- [#1412] Oyuncu etkinliği arka plandayken yeni amaç alındığında oyuncunun NPE'sine neden olan sabit tekrar modu.
+- Oyuncunun açılır pencereye küçültülmesinin, açılır pencere izni verilmediğinde oyuncuyu yok etmemesi düzeltildi.
From f22417e7e7b01bd04c37eb2c3e57c4cfa273a6fe Mon Sep 17 00:00:00 2001
From: Carlos Melero <10256660+carmebar@users.noreply.github.com>
Date: Fri, 24 Jun 2022 18:03:48 +0200
Subject: [PATCH 090/240] Add option to hide future videos in feed
---
.../schabi/newpipe/local/feed/FeedFragment.kt | 16 +++++++
.../newpipe/local/feed/FeedViewModel.kt | 43 +++++++++++++++----
.../main/res/drawable/ic_history_future.xml | 15 +++++++
app/src/main/res/menu/menu_feed_fragment.xml | 11 ++++-
app/src/main/res/values/settings_keys.xml | 1 +
app/src/main/res/values/strings.xml | 1 +
6 files changed, 77 insertions(+), 10 deletions(-)
create mode 100644 app/src/main/res/drawable/ic_history_future.xml
diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt
index b291aa03568..f0ebabd8545 100644
--- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt
+++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt
@@ -98,6 +98,7 @@ class FeedFragment : BaseStateFragment() {
private lateinit var groupAdapter: GroupieAdapter
@State @JvmField var showPlayedItems: Boolean = true
+ @State @JvmField var showFutureItems: Boolean = true
private var onSettingsChangeListener: SharedPreferences.OnSharedPreferenceChangeListener? = null
private var updateListViewModeOnResume = false
@@ -137,6 +138,7 @@ class FeedFragment : BaseStateFragment() {
val factory = FeedViewModel.Factory(requireContext(), groupId)
viewModel = ViewModelProvider(this, factory).get(FeedViewModel::class.java)
showPlayedItems = viewModel.getShowPlayedItemsFromPreferences()
+ showFutureItems = viewModel.getShowFutureItemsFromPreferences()
viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(::handleResult) }
groupAdapter = GroupieAdapter().apply {
@@ -212,6 +214,7 @@ class FeedFragment : BaseStateFragment() {
inflater.inflate(R.menu.menu_feed_fragment, menu)
updateTogglePlayedItemsButton(menu.findItem(R.id.menu_item_feed_toggle_played_items))
+ updateToggleFutureItemsButton(menu.findItem(R.id.menu_item_feed_toggle_future_items))
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
@@ -241,6 +244,11 @@ class FeedFragment : BaseStateFragment() {
updateTogglePlayedItemsButton(item)
viewModel.togglePlayedItems(showPlayedItems)
viewModel.saveShowPlayedItemsToPreferences(showPlayedItems)
+ } else if (item.itemId == R.id.menu_item_feed_toggle_future_items) {
+ showFutureItems = !item.isChecked
+ updateToggleFutureItemsButton(item)
+ viewModel.toggleFutureItems(showFutureItems)
+ viewModel.saveShowFutureItemsToPreferences(showFutureItems)
}
return super.onOptionsItemSelected(item)
@@ -280,6 +288,14 @@ class FeedFragment : BaseStateFragment() {
)
}
+ private fun updateToggleFutureItemsButton(menuItem: MenuItem) {
+ menuItem.isChecked = showFutureItems
+ menuItem.icon = AppCompatResources.getDrawable(
+ requireContext(),
+ if (showFutureItems) R.drawable.ic_history_future else R.drawable.ic_history
+ )
+ }
+
// //////////////////////////////////////////////////////////////////////////
// Handling
// //////////////////////////////////////////////////////////////////////////
diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt
index e21963c1651..87409ddae76 100644
--- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt
+++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt
@@ -9,7 +9,7 @@ import androidx.lifecycle.ViewModelProvider
import androidx.preference.PreferenceManager
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Flowable
-import io.reactivex.rxjava3.functions.Function4
+import io.reactivex.rxjava3.functions.Function5
import io.reactivex.rxjava3.processors.BehaviorProcessor
import io.reactivex.rxjava3.schedulers.Schedulers
import org.schabi.newpipe.R
@@ -28,7 +28,8 @@ import java.util.concurrent.TimeUnit
class FeedViewModel(
private val applicationContext: Context,
groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
- initialShowPlayedItems: Boolean = true
+ initialShowPlayedItems: Boolean = true,
+ initialShowFutureItems: Boolean = true
) : ViewModel() {
private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(applicationContext)
@@ -37,6 +38,11 @@ class FeedViewModel(
.startWithItem(initialShowPlayedItems)
.distinctUntilChanged()
+ private val toggleShowFutureItems = BehaviorProcessor.create()
+ private val toggleShowFutureItemsFlowable = toggleShowFutureItems
+ .startWithItem(initialShowFutureItems)
+ .distinctUntilChanged()
+
private val mutableStateLiveData = MutableLiveData()
val stateLiveData: LiveData = mutableStateLiveData
@@ -44,22 +50,24 @@ class FeedViewModel(
.combineLatest(
FeedEventManager.events(),
toggleShowPlayedItemsFlowable,
+ toggleShowFutureItemsFlowable,
feedDatabaseManager.notLoadedCount(groupId),
feedDatabaseManager.oldestSubscriptionUpdate(groupId),
- Function4 { t1: FeedEventManager.Event, t2: Boolean,
- t3: Long, t4: List ->
- return@Function4 CombineResultEventHolder(t1, t2, t3, t4.firstOrNull())
+ Function5 { t1: FeedEventManager.Event, t2: Boolean, t3: Boolean,
+ t4: Long, t5: List ->
+ return@Function5 CombineResultEventHolder(t1, t2, t3, t4, t5.firstOrNull())
}
)
.throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS)
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
- .map { (event, showPlayedItems, notLoadedCount, oldestUpdate) ->
+ .map { (event, showPlayedItems, showFutureItems, notLoadedCount, oldestUpdate) ->
val streamItems = if (event is SuccessResultEvent || event is IdleEvent)
feedDatabaseManager
.getStreams(groupId, showPlayedItems)
.blockingGet(arrayListOf())
+ .filter { s -> showFutureItems || s.stream.uploadDate?.isBefore(OffsetDateTime.now()) ?: true }
else
arrayListOf()
@@ -89,8 +97,9 @@ class FeedViewModel(
private data class CombineResultEventHolder(
val t1: FeedEventManager.Event,
val t2: Boolean,
- val t3: Long,
- val t4: OffsetDateTime?
+ val t3: Boolean,
+ val t4: Long,
+ val t5: OffsetDateTime?
)
private data class CombineResultDataHolder(
@@ -112,10 +121,25 @@ class FeedViewModel(
fun getShowPlayedItemsFromPreferences() = getShowPlayedItemsFromPreferences(applicationContext)
+ fun toggleFutureItems(showFutureItems: Boolean) {
+ toggleShowFutureItems.onNext(showFutureItems)
+ }
+
+ fun saveShowFutureItemsToPreferences(showFutureItems: Boolean) =
+ PreferenceManager.getDefaultSharedPreferences(applicationContext).edit {
+ this.putBoolean(applicationContext.getString(R.string.feed_show_future_items_key), showFutureItems)
+ this.apply()
+ }
+
+ fun getShowFutureItemsFromPreferences() = getShowFutureItemsFromPreferences(applicationContext)
+
companion object {
private fun getShowPlayedItemsFromPreferences(context: Context) =
PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(context.getString(R.string.feed_show_played_items_key), true)
+ private fun getShowFutureItemsFromPreferences(context: Context) =
+ PreferenceManager.getDefaultSharedPreferences(context)
+ .getBoolean(context.getString(R.string.feed_show_future_items_key), true)
}
class Factory(
@@ -128,7 +152,8 @@ class FeedViewModel(
context.applicationContext,
groupId,
// Read initial value from preferences
- getShowPlayedItemsFromPreferences(context.applicationContext)
+ getShowPlayedItemsFromPreferences(context.applicationContext),
+ getShowFutureItemsFromPreferences(context.applicationContext)
) as T
}
}
diff --git a/app/src/main/res/drawable/ic_history_future.xml b/app/src/main/res/drawable/ic_history_future.xml
new file mode 100644
index 00000000000..db6f2acbf9d
--- /dev/null
+++ b/app/src/main/res/drawable/ic_history_future.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/app/src/main/res/menu/menu_feed_fragment.xml b/app/src/main/res/menu/menu_feed_fragment.xml
index 7a948ea8abb..f6929a914d7 100644
--- a/app/src/main/res/menu/menu_feed_fragment.xml
+++ b/app/src/main/res/menu/menu_feed_fragment.xml
@@ -4,13 +4,22 @@
+
+
feed_update_threshold_key300feed_show_played_items
+ feed_show_future_itemsshow_thumbnail_key
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 6a1c220f0d5..1d65b6fbac7 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -747,4 +747,5 @@
Select quality for external playersUnknown formatUnknown quality
+ Show future videos
\ No newline at end of file
From dc7fce86a566b9360af4b855c06f4c08a3fce8be Mon Sep 17 00:00:00 2001
From: Stypox
Date: Fri, 24 Jun 2022 17:53:51 +0200
Subject: [PATCH 091/240] Add changelog for v0.23.1 (987)
---
fastlane/metadata/android/en-US/changelogs/987.txt | 12 ++++++++++++
1 file changed, 12 insertions(+)
create mode 100644 fastlane/metadata/android/en-US/changelogs/987.txt
diff --git a/fastlane/metadata/android/en-US/changelogs/987.txt b/fastlane/metadata/android/en-US/changelogs/987.txt
new file mode 100644
index 00000000000..c3404e2a22f
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/987.txt
@@ -0,0 +1,12 @@
+New
+• Support delivery methods other than progressive HTTP: faster playback loading time, fixes for PeerTube and SoundCloud, playback of recently-ended YouTube livestreams
+• Add button to add a remote playlist to a local one
+• Image preview in Android 10+ share sheet
+
+Improved
+• Improve playback parameters dialog
+• Move subscription import/export buttons to three-dot menu
+
+Fixed
+• Fix removing fully watched videos from playlist
+• Fix share menu theme and "add to playlist" entry
From 4ee1cd5826d7bbb9c755ce3dbcbbd6175144bc37 Mon Sep 17 00:00:00 2001
From: Stypox
Date: Fri, 24 Jun 2022 19:01:37 +0200
Subject: [PATCH 092/240] Release v0.23.1 (987)
---
app/build.gradle | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/app/build.gradle b/app/build.gradle
index 995dae6ed5f..b3d9986def9 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -16,8 +16,8 @@ android {
resValue "string", "app_name", "NewPipe"
minSdk 19
targetSdk 29
- versionCode 986
- versionName "0.23.0"
+ versionCode 987
+ versionName "0.23.1"
multiDexEnabled true
From d5985be94a7c631e21aa7e0c160013508d138c16 Mon Sep 17 00:00:00 2001
From: opusforlife2 <53176348+opusforlife2@users.noreply.github.com>
Date: Sat, 25 Jun 2022 22:13:54 +0000
Subject: [PATCH 093/240] Made some much needed changes to the ReadMe (#8372)
Co-authored-by: Mohammed Anas
Co-authored-by: Poolitzer
---
README.md | 120 +++++++++++++++++++++++++++---------------------------
1 file changed, 59 insertions(+), 61 deletions(-)
diff --git a/README.md b/README.md
index c47f8c2f4fb..d3a34816d59 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
NewPipe
-
A libre lightweight streaming frontend for Android.
+
A libre lightweight streaming front-end for Android.
*Read this in other languages: [English](README.md), [Español](doc/README.es.md), [हिन्दी](doc/README.hi.md), [한국어](doc/README.ko.md), [Soomaali](doc/README.so.md), [Português Brasil](doc/README.pt_BR.md), [Polski](doc/README.pl.md), [日本語](doc/README.ja.md), [Română](doc/README.ro.md), [Türkçe](doc/README.tr.md), [正體中文](doc/README.zh_TW.md).*
-WARNING: THIS IS A BETA VERSION, THEREFORE YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE VIA OUR GITHUB REPOSITORY.
+WARNING: THIS APP IS IN BETA, SO YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE IN OUR GITHUB REPOSITORY BY FILLING OUT THE ISSUE TEMPLATE.
-PUTTING NEWPIPE OR ANY FORK OF IT INTO THE GOOGLE PLAY STORE VIOLATES THEIR TERMS AND CONDITIONS.
+PUTTING NEWPIPE, OR ANY FORK OF IT, INTO THE GOOGLE PLAY STORE VIOLATES THEIR TERMS AND CONDITIONS.
## Screenshots
@@ -38,50 +38,53 @@
[](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png)
[](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png)
-## Description
+### Supported Services
-NewPipe does not use any Google framework libraries, nor the YouTube API. Websites are only parsed to fetch required info, so this app can be used on devices without Google services installed. Also, you don't need a YouTube account to use NewPipe, which is copylefted libre software.
+NewPipe currently supports these services:
-### Features
+
+* YouTube ([website](https://www.youtube.com/)) and YouTube Music ([website](https://music.youtube.com/)) ([wiki](https://en.wikipedia.org/wiki/YouTube))
+* PeerTube ([website](https://joinpeertube.org/)) and all its instances (open the website to know what that means!) ([wiki](https://en.wikipedia.org/wiki/PeerTube))
+* Bandcamp ([website](https://bandcamp.com/)) ([wiki](https://en.wikipedia.org/wiki/Bandcamp))
+* SoundCloud ([website](https://soundcloud.com/)) ([wiki](https://en.wikipedia.org/wiki/SoundCloud))
+* media.ccc.de ([website](https://media.ccc.de/)) ([wiki](https://en.wikipedia.org/wiki/Chaos_Computer_Club))
-* Search videos
-* No Login Required
-* Display general info about videos
-* Watch YouTube videos
-* Listen to YouTube videos
-* Popup mode (floating player)
-* Select streaming player to watch video with
-* Download videos
-* Download audio only
-* Open a video in Kodi
-* Show next/related videos
-* Search YouTube in a specific language
-* Watch/Block age restricted material
-* Display general info about channels
-* Search channels
-* Watch videos from a channel
-* Orbot/Tor support (not yet directly)
-* 1080p/2K/4K support
-* View history
-* Subscribe to channels
-* Search history
-* Search/watch playlists
-* Watch as enqueued playlists
-* Enqueue videos
-* Local playlists
-* Subtitles
-* Livestream support
-* Show comments
+As you can see, NewPipe supports multiple video and audio services. Though it started off with YouTube, other people have added more services over the years, making NewPipe more and more versatile!
-### Supported Services
+Partially due to circumstance, and partially due to its popularity, YouTube is the best supported out of these services. If you use or are familiar with any of these other services, please help us improve support for them! We're looking for maintainers for SoundCloud and PeerTube.
+
+If you intend to add a new service, please get in touch with us first! Our [docs](https://teamnewpipe.github.io/documentation/) provide more information on how a new service can be added to the app and to the [NewPipe Extractor](https://github.com/TeamNewPipe/NewPipeExtractor).
-NewPipe supports multiple services. Our [docs](https://teamnewpipe.github.io/documentation/) provide more info on how a new service can be added to the app and the extractor. Please get in touch with us if you intend to add a new one. Currently supported services are:
+## Description
-* YouTube
-* SoundCloud \[beta\]
-* media.ccc.de \[beta\]
-* PeerTube instances \[beta\]
-* Bandcamp \[beta\]
+NewPipe works by fetching the required data from the official API (e.g. PeerTube) of the service you're using. If the official API is restricted (e.g. YouTube) for our purposes, or is proprietary, the app parses the website or uses an internal API instead. This means that you don't need an account on any service to use NewPipe.
+
+Also, since they are free and open source software, neither the app nor the Extractor use any proprietary libraries or frameworks, such as Google Play Services. This means you can use NewPipe on devices or custom ROMs that do not have Google apps installed.
+
+### Features
+
+* Watch videos at resolutions up to 4K
+* Listen to audio in the background, only loading the audio stream to save data
+* Popup mode (floating player, aka Picture-in-Picture)
+* Watch live streams
+* Show/hide subtitles/closed captions
+* Search videos and audios (on YouTube, you can specify the content language as well)
+* Enqueue videos (and optionally save them as local playlists)
+* Show/hide general information about videos (such as description and tags)
+* Show/hide next/related videos
+* Show/hide comments
+* Search videos, audios, channels, playlists and albums
+* Browse videos and audios within a channel
+* Subscribe to channels (yes, without logging into any account!)
+* Get notifications about new videos from channels you're subscribed to
+* Create and edit channel groups (for easier browsing and management)
+* Browse video feeds generated from your channel groups
+* View and search your watch history
+* Search and watch playlists (these are remote playlists, which means they're fetched from the service you're browsing)
+* Create and edit local playlists (these are created and saved within the app, and have nothing to do with any service)
+* Download videos/audios/subtitles (closed captions)
+* Open in Kodi
+* Watch/Block age-restricted material
@@ -90,10 +93,11 @@ NewPipe supports multiple services. Our [docs](https://teamnewpipe.github.io/doc
You can install NewPipe using one of the following methods:
1. Add our custom repo to F-Droid and install it from there. The instructions are here: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/
2. Download the APK from [GitHub Releases](https://github.com/TeamNewPipe/NewPipe/releases) and install it.
- 3. Update via F-Droid. This is the slowest method of getting updates, as F-Droid must recognize changes, build the APK itself, sign it, then push the update to users.
+ 3. Update via F-Droid. This is the slowest method of getting updates, as F-Droid must recognize changes, build the APK itself, sign it, and then push the update to users.
4. Build a debug APK yourself. This is the fastest way to get new features on your device, but is much more complicated, so we recommend using one of the other methods.
+ 5. If you're interested in a specific feature or bugfix provided in a Pull Request in this repo, you can also download its APK from within the PR. Read the PR description for instructions. The great thing about PR-specific APKs is that they're installed side-by-side the official app, so you don't have to worry about losing your data or messing anything up.
-We recommend method 1 for most users. APKs installed using method 1 or 2 are compatible with each other, but not with those installed using method 3. This is due to the same signing key (ours) being used for 1 and 2, but a different signing key (F-Droid's) being used for 3. Building a debug APK using method 4 excludes a key entirely. Signing keys help ensure that a user isn't tricked into installing a malicious update to an app.
+We recommend method 1 for most users. APKs installed using method 1 or 2 are compatible with each other (meaning that if you installed NewPipe using either method 1 or 2, you can also update NewPipe using the other), but not with those installed using method 3. This is due to the same signing key (ours) being used for 1 and 2, but a different signing key (F-Droid's) being used for 3. Building a debug APK using method 4 excludes a key entirely. Signing keys help ensure that a user isn't tricked into installing a malicious update to an app. When using method 5, each APK is signed with a different random key supplied by GitHub Actions, so you cannot even update it. You will have to backup and restore the app data each time you wish to use a new APK.
In the meanwhile, if you want to switch sources for some reason (e.g. NewPipe's core functionality breaks and F-Droid doesn't have the latest update yet), we recommend following this procedure:
1. Back up your data via Settings > Content > Export Database so you keep your history, subscriptions, and playlists
@@ -101,30 +105,29 @@ In the meanwhile, if you want to switch sources for some reason (e.g. NewPipe's
3. Download the APK from the new source and install it
4. Import the data from step 1 via Settings > Content > Import Database
-## Contribution
-Whether you have ideas, translations, design changes, code cleaning, or real heavy code changes, help is always welcome.
-The more is done the better it gets!
+Note: when you're importing a database into the official app, always make sure that it is the one you exported _from_ the official app. If you import a database exported from an APK other than the official app, it may break things. Such an action is unsupported, and you should only do so when you're absolutely certain you know what you're doing.
-If you'd like to get involved, check our [contribution notes](.github/CONTRIBUTING.md).
+## Contribution
+Whether you have ideas, translations, design changes, code cleaning, or even major code changes, help is always welcome. The app gets better and better with each contribution, no matter how big or small! If you'd like to get involved, check our [contribution notes](.github/CONTRIBUTING.md).
## Donate
-If you like NewPipe we'd be happy about a donation. You can either send bitcoin or donate via Bountysource or Liberapay. For further info on donating to NewPipe, please visit our [website](https://newpipe.net/donate).
+If you like NewPipe, you're welcome to send a donation. We prefer Liberapay, as it is both open-source and non-profit. For further info on donating to NewPipe, please visit our [website](https://newpipe.net/donate).
- * This is necessary when the thumbnail's height is larger than the device's height
- * and thus is enlarging the player's height
- * causing the bottom playback controls to be out of the visible screen.
- *
- */
- public void updateEndScreenThumbnail() {
- if (currentThumbnail == null) {
- return;
- }
-
- final float endScreenHeight = calculateMaxEndScreenThumbnailHeight();
-
- final Bitmap endScreenBitmap = Bitmap.createScaledBitmap(
- currentThumbnail,
- (int) (currentThumbnail.getWidth()
- / (currentThumbnail.getHeight() / endScreenHeight)),
- (int) endScreenHeight,
- true);
-
- if (DEBUG) {
- Log.d(TAG, "Thumbnail - updateEndScreenThumbnail() called with: "
- + "currentThumbnail = [" + currentThumbnail + "], "
- + currentThumbnail.getWidth() + "x" + currentThumbnail.getHeight()
- + ", scaled end screen height = " + endScreenHeight
- + ", scaled end screen width = " + endScreenBitmap.getWidth());
- }
-
- binding.endScreen.setImageBitmap(endScreenBitmap);
- }
-
- /**
- * Calculate the maximum allowed height for the {@link R.id.endScreen}
- * to prevent it from enlarging the player.
- *
- * The calculating follows these rules:
- *
- *
- * Show at least stream title and content creator on TVs and tablets
- * when in landscape (always the case for TVs) and not in fullscreen mode.
- * This requires to have at least 85dp free space for {@link R.id.detail_root}
- * and additional space for the stream title text size
- * ({@link R.id.detail_title_root_layout}).
- * The text size is 15sp on tablets and 16sp on TVs,
- * see {@link R.id.titleTextView}.
- *
- *
- * Otherwise, the max thumbnail height is the screen height.
- *
- *
- *
- * @return the maximum height for the end screen thumbnail
- */
- private float calculateMaxEndScreenThumbnailHeight() {
- // ensure that screenHeight is initialized and thus not 0
- updateScreenSize();
-
- if (DeviceUtils.isTv(context) && !isFullscreen) {
- final int videoInfoHeight =
- DeviceUtils.dpToPx(85, context) + DeviceUtils.spToPx(16, context);
- return Math.min(currentThumbnail.getHeight(), screenHeight - videoInfoHeight);
- } else if (DeviceUtils.isTablet(context) && service.isLandscape() && !isFullscreen) {
- final int videoInfoHeight =
- DeviceUtils.dpToPx(85, context) + DeviceUtils.spToPx(15, context);
- return Math.min(currentThumbnail.getHeight(), screenHeight - videoInfoHeight);
- } else { // fullscreen player: max height is the device height
- return Math.min(currentThumbnail.getHeight(), screenHeight);
- }
- }
- //endregion
-
-
-
- /*//////////////////////////////////////////////////////////////////////////
- // Popup player utils
- //////////////////////////////////////////////////////////////////////////*/
- //region Popup player utils
-
- /**
- * Check if {@link #popupLayoutParams}' position is within a arbitrary boundary
- * that goes from (0, 0) to (screenWidth, screenHeight).
- *
- * If it's out of these boundaries, {@link #popupLayoutParams}' position is changed
- * and {@code true} is returned to represent this change.
- *
- */
- public void checkPopupPositionBounds() {
- if (DEBUG) {
- Log.d(TAG, "checkPopupPositionBounds() called with: "
- + "screenWidth = [" + screenWidth + "], "
- + "screenHeight = [" + screenHeight + "]");
- }
- if (popupLayoutParams == null) {
- return;
- }
-
- if (popupLayoutParams.x < 0) {
- popupLayoutParams.x = 0;
- } else if (popupLayoutParams.x > screenWidth - popupLayoutParams.width) {
- popupLayoutParams.x = (int) (screenWidth - popupLayoutParams.width);
- }
-
- if (popupLayoutParams.y < 0) {
- popupLayoutParams.y = 0;
- } else if (popupLayoutParams.y > screenHeight - popupLayoutParams.height) {
- popupLayoutParams.y = (int) (screenHeight - popupLayoutParams.height);
- }
- }
-
- public void updateScreenSize() {
- if (windowManager != null) {
- final DisplayMetrics metrics = new DisplayMetrics();
- windowManager.getDefaultDisplay().getMetrics(metrics);
-
- screenWidth = metrics.widthPixels;
- screenHeight = metrics.heightPixels;
- if (DEBUG) {
- Log.d(TAG, "updateScreenSize() called: screenWidth = ["
- + screenWidth + "], screenHeight = [" + screenHeight + "]");
- }
- }
- }
-
- /**
- * Changes the size of the popup based on the width.
- * @param width the new width, height is calculated with
- * {@link PlayerHelper#getMinimumVideoHeight(float)}
- */
- public void changePopupSize(final int width) {
- if (DEBUG) {
- Log.d(TAG, "changePopupSize() called with: width = [" + width + "]");
- }
-
- if (anyPopupViewIsNull()) {
- return;
- }
-
- final float minimumWidth = context.getResources().getDimension(R.dimen.popup_minimum_width);
- final int actualWidth = (int) (width > screenWidth ? screenWidth
- : (width < minimumWidth ? minimumWidth : width));
- final int actualHeight = (int) getMinimumVideoHeight(width);
- if (DEBUG) {
- Log.d(TAG, "updatePopupSize() updated values:"
- + " width = [" + actualWidth + "], height = [" + actualHeight + "]");
- }
-
- popupLayoutParams.width = actualWidth;
- popupLayoutParams.height = actualHeight;
- binding.surfaceView.setHeights(popupLayoutParams.height, popupLayoutParams.height);
- Objects.requireNonNull(windowManager)
- .updateViewLayout(binding.getRoot(), popupLayoutParams);
- }
-
- private void changePopupWindowFlags(final int flags) {
- if (DEBUG) {
- Log.d(TAG, "changePopupWindowFlags() called with: flags = [" + flags + "]");
- }
-
- if (!anyPopupViewIsNull()) {
- popupLayoutParams.flags = flags;
- Objects.requireNonNull(windowManager)
- .updateViewLayout(binding.getRoot(), popupLayoutParams);
- }
- }
-
- public void closePopup() {
- if (DEBUG) {
- Log.d(TAG, "closePopup() called, isPopupClosing = " + isPopupClosing);
- }
- if (isPopupClosing) {
- return;
- }
- isPopupClosing = true;
-
- saveStreamProgressState();
- Objects.requireNonNull(windowManager).removeView(binding.getRoot());
-
- animatePopupOverlayAndFinishService();
- }
-
- public void removePopupFromView() {
- if (windowManager != null) {
- // wrap in try-catch since it could sometimes generate errors randomly
- try {
- if (popupHasParent()) {
- windowManager.removeView(binding.getRoot());
- }
- } catch (final IllegalArgumentException e) {
- Log.w(TAG, "Failed to remove popup from window manager", e);
- }
-
- try {
- final boolean closeOverlayHasParent = closeOverlayBinding != null
- && closeOverlayBinding.getRoot().getParent() != null;
- if (closeOverlayHasParent) {
- windowManager.removeView(closeOverlayBinding.getRoot());
- }
- } catch (final IllegalArgumentException e) {
- Log.w(TAG, "Failed to remove popup overlay from window manager", e);
- }
- }
- }
-
- private void animatePopupOverlayAndFinishService() {
- final int targetTranslationY =
- (int) (closeOverlayBinding.closeButton.getRootView().getHeight()
- - closeOverlayBinding.closeButton.getY());
-
- closeOverlayBinding.closeButton.animate().setListener(null).cancel();
- closeOverlayBinding.closeButton.animate()
- .setInterpolator(new AnticipateInterpolator())
- .translationY(targetTranslationY)
- .setDuration(400)
- .setListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationCancel(final Animator animation) {
- end();
- }
-
- @Override
- public void onAnimationEnd(final Animator animation) {
- end();
- }
-
- private void end() {
- Objects.requireNonNull(windowManager)
- .removeView(closeOverlayBinding.getRoot());
- closeOverlayBinding = null;
- service.stopService();
- }
- }).start();
- }
-
- private boolean popupHasParent() {
- return binding != null
- && binding.getRoot().getLayoutParams() instanceof WindowManager.LayoutParams
- && binding.getRoot().getParent() != null;
- }
-
- private boolean anyPopupViewIsNull() {
- // TODO understand why checking getParentActivity() != null
- return popupLayoutParams == null || windowManager == null
- || getParentActivity() != null || binding.getRoot().getParent() == null;
- }
- //endregion
+ //endregion
@@ -1645,7 +828,7 @@ public float getPlaybackSpeed() {
return getPlaybackParameters().speed;
}
- private void setPlaybackSpeed(final float speed) {
+ public void setPlaybackSpeed(final float speed) {
setPlaybackParameters(speed, getPlaybackPitch(), getPlaybackSkipSilence());
}
@@ -1694,40 +877,13 @@ public void setPlaybackParameters(final float speed, final float pitch,
private void onUpdateProgress(final int currentProgress,
final int duration,
final int bufferPercent) {
- if (!isPrepared) {
- return;
- }
-
- if (duration != binding.playbackSeekBar.getMax()) {
- setVideoDurationToControls(duration);
- }
- if (currentState != STATE_PAUSED) {
- updatePlayBackElementsCurrentDuration(currentProgress);
- }
- if (simpleExoPlayer.isLoading() || bufferPercent > 90) {
- binding.playbackSeekBar.setSecondaryProgress(
- (int) (binding.playbackSeekBar.getMax() * ((float) bufferPercent / 100)));
- }
- if (DEBUG && bufferPercent % 20 == 0) { //Limit log
- Log.d(TAG, "notifyProgressUpdateToListeners() called with: "
- + "isVisible = " + isControlsVisible() + ", "
- + "currentProgress = [" + currentProgress + "], "
- + "duration = [" + duration + "], bufferPercent = [" + bufferPercent + "]");
- }
- binding.playbackLiveSync.setClickable(!isLiveEdge());
-
- notifyProgressUpdateToListeners(currentProgress, duration, bufferPercent);
-
- if (areSegmentsVisible) {
- segmentAdapter.selectSegmentAt(getNearestStreamSegmentPosition(currentProgress));
- }
-
- if (isQueueVisible) {
- updateQueueTime(currentProgress);
+ if (isPrepared) {
+ UIs.call(ui -> ui.onUpdateProgress(currentProgress, duration, bufferPercent));
+ notifyProgressUpdateToListeners(currentProgress, duration, bufferPercent);
}
}
- private void startProgressLoop() {
+ public void startProgressLoop() {
progressUpdateDisposable.set(getProgressUpdateDisposable());
}
@@ -1735,11 +891,11 @@ private void stopProgressLoop() {
progressUpdateDisposable.set(null);
}
- private boolean isProgressLoopRunning() {
+ public boolean isProgressLoopRunning() {
return progressUpdateDisposable.get() != null;
}
- private void triggerProgressUpdate() {
+ public void triggerProgressUpdate() {
if (exoPlayerIsNull()) {
return;
}
@@ -1756,228 +912,12 @@ private Disposable getProgressUpdateDisposable() {
error -> Log.e(TAG, "Progress update failure: ", error));
}
- @Override // seekbar listener
- public void onProgressChanged(final SeekBar seekBar, final int progress,
- final boolean fromUser) {
- // Currently we don't need method execution when fromUser is false
- if (!fromUser) {
- return;
- }
- if (DEBUG) {
- Log.d(TAG, "onProgressChanged() called with: "
- + "seekBar = [" + seekBar + "], progress = [" + progress + "]");
- }
-
- binding.currentDisplaySeek.setText(getTimeString(progress));
-
- // Seekbar Preview Thumbnail
- SeekbarPreviewThumbnailHelper
- .tryResizeAndSetSeekbarPreviewThumbnail(
- getContext(),
- seekbarPreviewThumbnailHolder.getBitmapAt(progress),
- binding.currentSeekbarPreviewThumbnail,
- binding.subtitleView::getWidth);
-
- adjustSeekbarPreviewContainer();
- }
-
- private void adjustSeekbarPreviewContainer() {
- try {
- // Should only be required when an error occurred before
- // and the layout was positioned in the center
- binding.bottomSeekbarPreviewLayout.setGravity(Gravity.NO_GRAVITY);
-
- // Calculate the current left position of seekbar progress in px
- // More info: https://stackoverflow.com/q/20493577
- final int currentSeekbarLeft =
- binding.playbackSeekBar.getLeft()
- + binding.playbackSeekBar.getPaddingLeft()
- + binding.playbackSeekBar.getThumb().getBounds().left;
-
- // Calculate the (unchecked) left position of the container
- final int uncheckedContainerLeft =
- currentSeekbarLeft - (binding.seekbarPreviewContainer.getWidth() / 2);
-
- // Fix the position so it's within the boundaries
- final int checkedContainerLeft =
- Math.max(
- Math.min(
- uncheckedContainerLeft,
- // Max left
- binding.playbackWindowRoot.getWidth()
- - binding.seekbarPreviewContainer.getWidth()
- ),
- 0 // Min left
- );
-
- // See also: https://stackoverflow.com/a/23249734
- final LinearLayout.LayoutParams params =
- new LinearLayout.LayoutParams(
- binding.seekbarPreviewContainer.getLayoutParams());
- params.setMarginStart(checkedContainerLeft);
- binding.seekbarPreviewContainer.setLayoutParams(params);
- } catch (final Exception ex) {
- Log.e(TAG, "Failed to adjust seekbarPreviewContainer", ex);
- // Fallback - position in the middle
- binding.bottomSeekbarPreviewLayout.setGravity(Gravity.CENTER);
- }
- }
-
- @Override // seekbar listener
- public void onStartTrackingTouch(final SeekBar seekBar) {
- if (DEBUG) {
- Log.d(TAG, "onStartTrackingTouch() called with: seekBar = [" + seekBar + "]");
- }
- if (currentState != STATE_PAUSED_SEEK) {
- changeState(STATE_PAUSED_SEEK);
- }
-
- saveWasPlaying();
- if (isPlaying()) {
- simpleExoPlayer.pause();
- }
-
- showControls(0);
- animate(binding.currentDisplaySeek, true, DEFAULT_CONTROLS_DURATION,
- AnimationType.SCALE_AND_ALPHA);
- animate(binding.currentSeekbarPreviewThumbnail, true, DEFAULT_CONTROLS_DURATION,
- AnimationType.SCALE_AND_ALPHA);
- }
-
- @Override // seekbar listener
- public void onStopTrackingTouch(final SeekBar seekBar) {
- if (DEBUG) {
- Log.d(TAG, "onStopTrackingTouch() called with: seekBar = [" + seekBar + "]");
- }
-
- seekTo(seekBar.getProgress());
- if (wasPlaying || simpleExoPlayer.getDuration() == seekBar.getProgress()) {
- simpleExoPlayer.play();
- }
-
- binding.playbackCurrentTime.setText(getTimeString(seekBar.getProgress()));
- animate(binding.currentDisplaySeek, false, 200, AnimationType.SCALE_AND_ALPHA);
- animate(binding.currentSeekbarPreviewThumbnail, false, 200, AnimationType.SCALE_AND_ALPHA);
-
- if (currentState == STATE_PAUSED_SEEK) {
- changeState(STATE_BUFFERING);
- }
- if (!isProgressLoopRunning()) {
- startProgressLoop();
- }
- if (wasPlaying) {
- showControlsThenHide();
- }
- }
-
public void saveWasPlaying() {
this.wasPlaying = getPlayWhenReady();
}
- //endregion
-
-
-
- /*//////////////////////////////////////////////////////////////////////////
- // Controls showing / hiding
- //////////////////////////////////////////////////////////////////////////*/
- //region Controls showing / hiding
-
- public boolean isControlsVisible() {
- return binding != null && binding.playbackControlRoot.getVisibility() == View.VISIBLE;
- }
-
- public void showControlsThenHide() {
- if (DEBUG) {
- Log.d(TAG, "showControlsThenHide() called");
- }
- showOrHideButtons();
- showSystemUIPartially();
-
- final int hideTime = binding.playbackControlRoot.isInTouchMode()
- ? DEFAULT_CONTROLS_HIDE_TIME
- : DPAD_CONTROLS_HIDE_TIME;
-
- showHideShadow(true, DEFAULT_CONTROLS_DURATION);
- animate(binding.playbackControlRoot, true, DEFAULT_CONTROLS_DURATION,
- AnimationType.ALPHA, 0, () -> hideControls(DEFAULT_CONTROLS_DURATION, hideTime));
- }
-
- public void showControls(final long duration) {
- if (DEBUG) {
- Log.d(TAG, "showControls() called");
- }
- showOrHideButtons();
- showSystemUIPartially();
- controlsVisibilityHandler.removeCallbacksAndMessages(null);
- showHideShadow(true, duration);
- animate(binding.playbackControlRoot, true, duration);
- }
-
- public void hideControls(final long duration, final long delay) {
- if (DEBUG) {
- Log.d(TAG, "hideControls() called with: duration = [" + duration
- + "], delay = [" + delay + "]");
- }
-
- showOrHideButtons();
-
- controlsVisibilityHandler.removeCallbacksAndMessages(null);
- controlsVisibilityHandler.postDelayed(() -> {
- showHideShadow(false, duration);
- animate(binding.playbackControlRoot, false, duration, AnimationType.ALPHA,
- 0, this::hideSystemUIIfNeeded);
- }, delay);
- }
-
- public void showHideShadow(final boolean show, final long duration) {
- animate(binding.playbackControlsShadow, show, duration, AnimationType.ALPHA, 0, null);
- animate(binding.playerTopShadow, show, duration, AnimationType.ALPHA, 0, null);
- animate(binding.playerBottomShadow, show, duration, AnimationType.ALPHA, 0, null);
- }
-
- private void showOrHideButtons() {
- if (playQueue == null) {
- return;
- }
-
- final boolean showPrev = playQueue.getIndex() != 0;
- final boolean showNext = playQueue.getIndex() + 1 != playQueue.getStreams().size();
- final boolean showQueue = playQueue.getStreams().size() > 1 && !popupPlayerSelected();
- /* only when stream has segments and is not playing in popup player */
- final boolean showSegment = !popupPlayerSelected()
- && !getCurrentStreamInfo()
- .map(StreamInfo::getStreamSegments)
- .map(List::isEmpty)
- .orElse(/*no stream info=*/true);
-
- binding.playPreviousButton.setVisibility(showPrev ? View.VISIBLE : View.INVISIBLE);
- binding.playPreviousButton.setAlpha(showPrev ? 1.0f : 0.0f);
- binding.playNextButton.setVisibility(showNext ? View.VISIBLE : View.INVISIBLE);
- binding.playNextButton.setAlpha(showNext ? 1.0f : 0.0f);
- binding.queueButton.setVisibility(showQueue ? View.VISIBLE : View.GONE);
- binding.queueButton.setAlpha(showQueue ? 1.0f : 0.0f);
- binding.segmentsButton.setVisibility(showSegment ? View.VISIBLE : View.GONE);
- binding.segmentsButton.setAlpha(showSegment ? 1.0f : 0.0f);
- }
-
- private void showSystemUIPartially() {
- final AppCompatActivity activity = getParentActivity();
- if (isFullscreen && activity != null) {
- activity.getWindow().setStatusBarColor(Color.TRANSPARENT);
- activity.getWindow().setNavigationBarColor(Color.TRANSPARENT);
-
- final int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
- | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
- | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
- activity.getWindow().getDecorView().setSystemUiVisibility(visibility);
- activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
- }
- }
- private void hideSystemUIIfNeeded() {
- if (fragmentListener != null) {
- fragmentListener.hideSystemUiIfNeeded();
- }
+ public boolean wasPlaying() {
+ return wasPlaying;
}
//endregion
@@ -2011,7 +951,7 @@ public void onPlaybackStateChanged(final int playbackState) {
private void updatePlaybackState(final boolean playWhenReady, final int playbackState) {
if (DEBUG) {
- Log.d(TAG, "ExoPlayer - onPlayerStateChanged() called with: "
+ Log.d(TAG, "ExoPlayer - updatePlaybackState() called with: "
+ "playWhenReady = [" + playWhenReady + "], "
+ "playbackState = [" + playbackState + "]");
}
@@ -2122,9 +1062,7 @@ private void onPrepared(final boolean playWhenReady) {
Log.d(TAG, "onPrepared() called with: playWhenReady = [" + playWhenReady + "]");
}
- setVideoDurationToControls((int) simpleExoPlayer.getDuration());
-
- binding.playbackSpeed.setText(formatSpeed(getPlaybackSpeed()));
+ UIs.call(PlayerUi::onPrepared);
if (playWhenReady) {
audioReactor.requestAudioFocus();
@@ -2139,20 +1077,7 @@ private void onBlocked() {
startProgressLoop();
}
- // if we are e.g. switching players, hide controls
- hideControls(DEFAULT_CONTROLS_DURATION, 0);
-
- binding.playbackSeekBar.setEnabled(false);
- binding.playbackSeekBar.getThumb()
- .setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.SRC_IN));
-
- binding.loadingPanel.setBackgroundColor(Color.BLACK);
- animate(binding.loadingPanel, true, 0);
- animate(binding.surfaceForeground, true, 100);
-
- binding.playPauseButton.setImageResource(R.drawable.ic_play_arrow);
- animatePlayButtons(false, 100);
- binding.getRoot().setKeepScreenOn(false);
+ UIs.call(PlayerUi::onBlocked);
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
}
@@ -2165,28 +1090,7 @@ private void onPlaying() {
startProgressLoop();
}
- updateStreamRelatedViews();
-
- binding.playbackSeekBar.setEnabled(true);
- binding.playbackSeekBar.getThumb()
- .setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.SRC_IN));
-
- binding.loadingPanel.setVisibility(View.GONE);
-
- animate(binding.currentDisplaySeek, false, 200, AnimationType.SCALE_AND_ALPHA);
-
- animate(binding.playPauseButton, false, 80, AnimationType.SCALE_AND_ALPHA, 0,
- () -> {
- binding.playPauseButton.setImageResource(R.drawable.ic_pause);
- animatePlayButtons(true, 200);
- if (!isQueueVisible) {
- binding.playPauseButton.requestFocus();
- }
- });
-
- changePopupWindowFlags(ONGOING_PLAYBACK_WINDOW_FLAGS);
- checkLandscape();
- binding.getRoot().setKeepScreenOn(true);
+ UIs.call(PlayerUi::onPlaying);
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
}
@@ -2195,10 +1099,8 @@ private void onBuffering() {
if (DEBUG) {
Log.d(TAG, "onBuffering() called");
}
- binding.loadingPanel.setBackgroundColor(Color.TRANSPARENT);
- binding.loadingPanel.setVisibility(View.VISIBLE);
- binding.getRoot().setKeepScreenOn(true);
+ UIs.call(PlayerUi::onBuffering);
if (NotificationUtil.getInstance().shouldUpdateBufferingSlot()) {
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
@@ -2214,22 +1116,7 @@ private void onPaused() {
stopProgressLoop();
}
- // Don't let UI elements popup during double tap seeking. This state is entered sometimes
- // during seeking/loading. This if-else check ensures that the controls aren't popping up.
- if (!playerGestureListener.isDoubleTapping()) {
- showControls(400);
- binding.loadingPanel.setVisibility(View.GONE);
-
- animate(binding.playPauseButton, false, 80, AnimationType.SCALE_AND_ALPHA, 0,
- () -> {
- binding.playPauseButton.setImageResource(R.drawable.ic_play_arrow);
- animatePlayButtons(true, 200);
- if (!isQueueVisible) {
- binding.playPauseButton.requestFocus();
- }
- });
- }
- changePopupWindowFlags(IDLE_WINDOW_FLAGS);
+ UIs.call(PlayerUi::onPaused);
// Remove running notification when user does not want minimization to background or popup
if (PlayerHelper.getMinimizeOnExitAction(context) == MINIMIZE_ON_EXIT_MODE_NONE
@@ -2238,8 +1125,6 @@ && videoPlayerSelected()) {
} else {
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
}
-
- binding.getRoot().setKeepScreenOn(false);
}
private void onPausedSeek() {
@@ -2247,8 +1132,7 @@ private void onPausedSeek() {
Log.d(TAG, "onPausedSeek() called");
}
- animatePlayButtons(false, 100);
- binding.getRoot().setKeepScreenOn(true);
+ UIs.call(PlayerUi::onPausedSeek);
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
}
@@ -2261,19 +1145,8 @@ private void onCompleted() {
return;
}
- animate(binding.playPauseButton, false, 0, AnimationType.SCALE_AND_ALPHA, 0,
- () -> {
- binding.playPauseButton.setImageResource(R.drawable.ic_replay);
- animatePlayButtons(true, DEFAULT_CONTROLS_DURATION);
- });
-
- binding.getRoot().setKeepScreenOn(false);
- changePopupWindowFlags(IDLE_WINDOW_FLAGS);
-
+ UIs.call(PlayerUi::onCompleted);
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
- if (isFullscreen) {
- toggleFullscreen();
- }
if (playQueue.getIndex() < playQueue.size() - 1) {
playQueue.offsetIndex(+1);
@@ -2281,38 +1154,6 @@ private void onCompleted() {
if (isProgressLoopRunning()) {
stopProgressLoop();
}
-
- // When a (short) video ends the elements have to display the correct values - see #6180
- updatePlayBackElementsCurrentDuration(binding.playbackSeekBar.getMax());
-
- showControls(500);
- animate(binding.currentDisplaySeek, false, 200, AnimationType.SCALE_AND_ALPHA);
- binding.loadingPanel.setVisibility(View.GONE);
- animate(binding.surfaceForeground, true, 100);
- }
-
- private void animatePlayButtons(final boolean show, final int duration) {
- animate(binding.playPauseButton, show, duration, AnimationType.SCALE_AND_ALPHA);
-
- boolean showQueueButtons = show;
- if (playQueue == null) {
- showQueueButtons = false;
- }
-
- if (!showQueueButtons || playQueue.getIndex() > 0) {
- animate(
- binding.playPreviousButton,
- showQueueButtons,
- duration,
- AnimationType.SCALE_AND_ALPHA);
- }
- if (!showQueueButtons || playQueue.getIndex() + 1 < playQueue.getStreams().size()) {
- animate(
- binding.playNextButton,
- showQueueButtons,
- duration,
- AnimationType.SCALE_AND_ALPHA);
- }
}
//endregion
@@ -2323,34 +1164,20 @@ private void animatePlayButtons(final boolean show, final int duration) {
//////////////////////////////////////////////////////////////////////////*/
//region Repeat and shuffle
- public void onRepeatClicked() {
- if (DEBUG) {
- Log.d(TAG, "onRepeatClicked() called");
- }
- setRepeatMode(nextRepeatMode(getRepeatMode()));
- }
-
- public void onShuffleClicked() {
- if (DEBUG) {
- Log.d(TAG, "onShuffleClicked() called");
- }
-
- if (exoPlayerIsNull()) {
- return;
- }
- simpleExoPlayer.setShuffleModeEnabled(!simpleExoPlayer.getShuffleModeEnabled());
- }
-
@RepeatMode
public int getRepeatMode() {
return exoPlayerIsNull() ? REPEAT_MODE_OFF : simpleExoPlayer.getRepeatMode();
}
- private void setRepeatMode(@RepeatMode final int repeatMode) {
+ public void setRepeatMode(@RepeatMode final int repeatMode) {
if (!exoPlayerIsNull()) {
simpleExoPlayer.setRepeatMode(repeatMode);
}
}
+
+ public void cycleNextRepeatMode() {
+ setRepeatMode(nextRepeatMode(getRepeatMode()));
+ }
@Override
public void onRepeatModeChanged(@RepeatMode final int repeatMode) {
@@ -2358,7 +1185,7 @@ public void onRepeatModeChanged(@RepeatMode final int repeatMode) {
Log.d(TAG, "ExoPlayer - onRepeatModeChanged() called with: "
+ "repeatMode = [" + repeatMode + "]");
}
- setRepeatModeButton(binding.repeatButton, repeatMode);
+ UIs.call(playerUi -> playerUi.onRepeatModeChanged(repeatMode));
onShuffleOrRepeatModeChanged();
}
@@ -2377,39 +1204,26 @@ public void onShuffleModeEnabledChanged(final boolean shuffleModeEnabled) {
}
}
- setShuffleButton(binding.shuffleButton, shuffleModeEnabled);
+ UIs.call(playerUi -> playerUi.onShuffleModeEnabledChanged(shuffleModeEnabled));
onShuffleOrRepeatModeChanged();
}
+
+ public void toggleShuffleModeEnabled() {
+ if (!exoPlayerIsNull()) {
+ simpleExoPlayer.setShuffleModeEnabled(!simpleExoPlayer.getShuffleModeEnabled());
+ }
+ }
private void onShuffleOrRepeatModeChanged() {
notifyPlaybackUpdateToListeners();
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
}
-
- private void setRepeatModeButton(final AppCompatImageButton imageButton,
- @RepeatMode final int repeatMode) {
- switch (repeatMode) {
- case REPEAT_MODE_OFF:
- imageButton.setImageResource(R.drawable.exo_controls_repeat_off);
- break;
- case REPEAT_MODE_ONE:
- imageButton.setImageResource(R.drawable.exo_controls_repeat_one);
- break;
- case REPEAT_MODE_ALL:
- imageButton.setImageResource(R.drawable.exo_controls_repeat_all);
- break;
- }
- }
-
- private void setShuffleButton(@NonNull final ImageButton button, final boolean shuffled) {
- button.setImageAlpha(shuffled ? 255 : 77);
- }
//endregion
/*//////////////////////////////////////////////////////////////////////////
- // Playlist append
+ // Playlist append TODO this does not make sense here
//////////////////////////////////////////////////////////////////////////*/
//region Playlist append
@@ -2439,23 +1253,16 @@ public void onAddToPlaylistClicked(@NonNull final FragmentManager fragmentManage
//////////////////////////////////////////////////////////////////////////*/
//region Mute / Unmute
- public void onMuteUnmuteButtonClicked() {
- if (DEBUG) {
- Log.d(TAG, "onMuteUnmuteButtonClicked() called");
- }
- simpleExoPlayer.setVolume(isMuted() ? 1 : 0);
+ public void toggleMute() {
+ final boolean wasMuted = isMuted();
+ simpleExoPlayer.setVolume(wasMuted ? 1 : 0);
+ UIs.call(playerUi -> playerUi.onMuteUnmuteChanged(!wasMuted));
notifyPlaybackUpdateToListeners();
- setMuteButton(binding.switchMute, isMuted());
}
- boolean isMuted() {
+ public boolean isMuted() {
return !exoPlayerIsNull() && simpleExoPlayer.getVolume() == 0;
}
-
- private void setMuteButton(@NonNull final ImageButton button, final boolean isMuted) {
- button.setImageDrawable(AppCompatResources.getDrawable(context, isMuted
- ? R.drawable.ic_volume_off : R.drawable.ic_volume_up));
- }
//endregion
@@ -2519,7 +1326,7 @@ public void onTracksChanged(@NonNull final Tracks tracks) {
Log.d(TAG, "ExoPlayer - onTracksChanged(), "
+ "track group size = " + tracks.getGroups().size());
}
- onTextTracksChanged(tracks);
+ UIs.call(playerUi -> playerUi.onTextTracksChanged(tracks));
}
@Override
@@ -2528,7 +1335,7 @@ public void onPlaybackParametersChanged(@NonNull final PlaybackParameters playba
Log.d(TAG, "ExoPlayer - playbackParameters(), speed = [" + playbackParameters.speed
+ "], pitch = [" + playbackParameters.pitch + "]");
}
- binding.playbackSpeed.setText(formatSpeed(playbackParameters.speed));
+ UIs.call(playerUi -> playerUi.onPlaybackParametersChanged(playbackParameters));
}
@Override
@@ -2580,13 +1387,12 @@ public void onPositionDiscontinuity(@NonNull final PositionInfo oldPosition,
@Override
public void onRenderedFirstFrame() {
- //TODO check if this causes black screen when switching to fullscreen
- animate(binding.surfaceForeground, false, DEFAULT_CONTROLS_DURATION);
+ UIs.call(PlayerUi::onRenderedFirstFrame);
}
@Override
public void onCues(@NonNull final CueGroup cueGroup) {
- binding.subtitleView.setCues(cueGroup.cues);
+ UIs.call(playerUi -> playerUi.onCues(cueGroup.cues));
}
//endregion
@@ -2627,7 +1433,7 @@ public void onCues(@NonNull final CueGroup cueGroup) {
// 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.
- @SuppressLint("SwitchIntDef")
+ @SuppressWarnings("SwitchIntDef")
@Override
public void onPlayerError(@NonNull final PlaybackException error) {
Log.e(TAG, "ExoPlayer - onPlayerError() called with:", error);
@@ -2706,18 +1512,6 @@ private void createErrorNotification(@NonNull final PlaybackException error) {
//////////////////////////////////////////////////////////////////////////*/
//region Playback position and seek
- /**
- * Sets the current duration into the corresponding elements.
- * @param currentProgress
- */
- private void updatePlayBackElementsCurrentDuration(final int currentProgress) {
- // Don't set seekbar progress while user is seeking
- if (currentState != STATE_PAUSED_SEEK) {
- binding.playbackSeekBar.setProgress(currentProgress);
- }
- binding.playbackCurrentTime.setText(getTimeString(currentProgress));
- }
-
@Override // own playback listener (this is a getter)
public boolean isApproachingPlaybackEdge(final long timeToEndMillis) {
// If live, then not near playback edge
@@ -2835,20 +1629,6 @@ public void seekToDefault() {
simpleExoPlayer.seekToDefaultPosition();
}
}
-
- /**
- * Sets the video duration time into all control components (e.g. seekbar).
- * @param duration
- */
- private void setVideoDurationToControls(final int duration) {
- binding.playbackEndTime.setText(getTimeString(duration));
-
- binding.playbackSeekBar.setMax(duration);
- // This is important for Android TVs otherwise it would apply the default from
- // setMax/Min methods which is (max - min) / 20
- binding.playbackSeekBar.setKeyProgressIncrement(
- PlayerHelper.retrieveSeekDurationFromPreferences(this));
- }
//endregion
@@ -2972,6 +1752,7 @@ private void registerStreamViewed() {
}
private void saveStreamProgressState(final long progressMillis) {
+ //noinspection SimplifyOptionalCallChains
if (!getCurrentStreamInfo().isPresent()
|| !prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)) {
return;
@@ -3026,17 +1807,10 @@ private void onMetadataChanged(@NonNull final StreamInfo info) {
Log.d(TAG, "Playback - onMetadataChanged() called, playing: " + info.getName());
}
+ UIs.call(playerUi -> playerUi.onMetadataChanged(info));
+
initThumbnail(info.getThumbnailUrl());
registerStreamViewed();
- updateStreamRelatedViews();
- showHideKodiButton();
-
- binding.titleTextView.setText(info.getName());
- binding.channelTextView.setText(info.getUploaderName());
-
- this.seekbarPreviewThumbnailHolder.resetFrom(this.getContext(), info.getPreviewFrames());
-
- NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
final boolean showThumbnail = prefs.getBoolean(
context.getString(R.string.show_thumbnail_key), true);
@@ -3048,17 +1822,7 @@ private void onMetadataChanged(@NonNull final StreamInfo info) {
);
notifyMetadataUpdateToListeners();
-
- if (areSegmentsVisible) {
- if (segmentAdapter.setItems(info)) {
- final int adapterPosition = getNearestStreamSegmentPosition(
- simpleExoPlayer.getCurrentPosition());
- segmentAdapter.selectSegmentAt(adapterPosition);
- binding.itemsList.scrollToPosition(adapterPosition);
- } else {
- closeItemsList();
- }
- }
+ NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
}
private void updateMetadataWith(@NonNull final StreamInfo streamInfo) {
@@ -3072,15 +1836,15 @@ private void updateMetadataWith(@NonNull final StreamInfo streamInfo) {
}
@NonNull
- private String getVideoUrl() {
+ public String getVideoUrl() {
return currentMetadata == null
? context.getString(R.string.unknown_content)
: currentMetadata.getStreamUrl();
}
@NonNull
- private String getVideoUrlAtCurrentTime() {
- final int timeSeconds = binding.playbackSeekBar.getProgress() / 1000;
+ public String getVideoUrlAtCurrentTime() {
+ final long timeSeconds = simpleExoPlayer.getCurrentPosition() / 1000;
String videoUrl = getVideoUrl();
if (!isLive() && timeSeconds >= 0 && currentMetadata != null
&& currentMetadata.getServiceId() == YouTube.getServiceId()) {
@@ -3156,190 +1920,10 @@ public void selectQueueItem(final PlayQueueItem item) {
@Override
public void onPlayQueueEdited() {
notifyPlaybackUpdateToListeners();
- showOrHideButtons();
+ UIs.call(PlayerUi::onPlayQueueEdited);
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
}
- private void onQueueClicked() {
- isQueueVisible = true;
-
- hideSystemUIIfNeeded();
- buildQueue();
-
- binding.itemsListHeaderTitle.setVisibility(View.GONE);
- binding.itemsListHeaderDuration.setVisibility(View.VISIBLE);
- binding.shuffleButton.setVisibility(View.VISIBLE);
- binding.repeatButton.setVisibility(View.VISIBLE);
- binding.addToPlaylistButton.setVisibility(View.VISIBLE);
-
- hideControls(0, 0);
- binding.itemsListPanel.requestFocus();
- animate(binding.itemsListPanel, true, DEFAULT_CONTROLS_DURATION,
- AnimationType.SLIDE_AND_ALPHA);
-
- binding.itemsList.scrollToPosition(playQueue.getIndex());
-
- updateQueueTime((int) simpleExoPlayer.getCurrentPosition());
- }
-
- private void buildQueue() {
- binding.itemsList.setAdapter(playQueueAdapter);
- binding.itemsList.setClickable(true);
- binding.itemsList.setLongClickable(true);
-
- binding.itemsList.clearOnScrollListeners();
- binding.itemsList.addOnScrollListener(getQueueScrollListener());
-
- itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
- itemTouchHelper.attachToRecyclerView(binding.itemsList);
-
- playQueueAdapter.setSelectedListener(getOnSelectedListener());
-
- binding.itemsListClose.setOnClickListener(view -> closeItemsList());
- }
-
- private void onSegmentsClicked() {
- areSegmentsVisible = true;
-
- hideSystemUIIfNeeded();
- buildSegments();
-
- binding.itemsListHeaderTitle.setVisibility(View.VISIBLE);
- binding.itemsListHeaderDuration.setVisibility(View.GONE);
- binding.shuffleButton.setVisibility(View.GONE);
- binding.repeatButton.setVisibility(View.GONE);
- binding.addToPlaylistButton.setVisibility(View.GONE);
-
- hideControls(0, 0);
- binding.itemsListPanel.requestFocus();
- animate(binding.itemsListPanel, true, DEFAULT_CONTROLS_DURATION,
- AnimationType.SLIDE_AND_ALPHA);
-
- final int adapterPosition = getNearestStreamSegmentPosition(simpleExoPlayer
- .getCurrentPosition());
- segmentAdapter.selectSegmentAt(adapterPosition);
- binding.itemsList.scrollToPosition(adapterPosition);
- }
-
- private void buildSegments() {
- binding.itemsList.setAdapter(segmentAdapter);
- binding.itemsList.setClickable(true);
- binding.itemsList.setLongClickable(false);
-
- binding.itemsList.clearOnScrollListeners();
- if (itemTouchHelper != null) {
- itemTouchHelper.attachToRecyclerView(null);
- }
-
- getCurrentStreamInfo().ifPresent(segmentAdapter::setItems);
-
- binding.shuffleButton.setVisibility(View.GONE);
- binding.repeatButton.setVisibility(View.GONE);
- binding.addToPlaylistButton.setVisibility(View.GONE);
- binding.itemsListClose.setOnClickListener(view -> closeItemsList());
- }
-
- public void closeItemsList() {
- if (isQueueVisible || areSegmentsVisible) {
- isQueueVisible = false;
- areSegmentsVisible = false;
-
- if (itemTouchHelper != null) {
- itemTouchHelper.attachToRecyclerView(null);
- }
-
- animate(binding.itemsListPanel, false, DEFAULT_CONTROLS_DURATION,
- AnimationType.SLIDE_AND_ALPHA, 0, () -> {
- // Even when queueLayout is GONE it receives touch events
- // and ruins normal behavior of the app. This line fixes it
- binding.itemsListPanel.setTranslationY(
- -binding.itemsListPanel.getHeight() * 5);
- });
-
- // clear focus, otherwise a white rectangle remains on top of the player
- binding.itemsListClose.clearFocus();
- binding.playPauseButton.requestFocus();
- }
- }
-
- private OnScrollBelowItemsListener getQueueScrollListener() {
- return new OnScrollBelowItemsListener() {
- @Override
- public void onScrolledDown(final RecyclerView recyclerView) {
- if (playQueue != null && !playQueue.isComplete()) {
- playQueue.fetch();
- } else if (binding != null) {
- binding.itemsList.clearOnScrollListeners();
- }
- }
- };
- }
-
- private StreamSegmentAdapter.StreamSegmentListener getStreamSegmentListener() {
- return (item, seconds) -> {
- segmentAdapter.selectSegment(item);
- seekTo(seconds * 1000L);
- triggerProgressUpdate();
- };
- }
-
- private int getNearestStreamSegmentPosition(final long playbackPosition) {
- int nearestPosition = 0;
- final List segments = getCurrentStreamInfo()
- .map(StreamInfo::getStreamSegments)
- .orElse(Collections.emptyList());
-
- for (int i = 0; i < segments.size(); i++) {
- if (segments.get(i).getStartTimeSeconds() * 1000L > playbackPosition) {
- break;
- }
- nearestPosition++;
- }
- return Math.max(0, nearestPosition - 1);
- }
-
- private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
- return new PlayQueueItemTouchCallback() {
- @Override
- public void onMove(final int sourceIndex, final int targetIndex) {
- if (playQueue != null) {
- playQueue.move(sourceIndex, targetIndex);
- }
- }
-
- @Override
- public void onSwiped(final int index) {
- if (index != -1) {
- playQueue.remove(index);
- }
- }
- };
- }
-
- private PlayQueueItemBuilder.OnSelectedListener getOnSelectedListener() {
- return new PlayQueueItemBuilder.OnSelectedListener() {
- @Override
- public void selected(final PlayQueueItem item, final View view) {
- selectQueueItem(item);
- }
-
- @Override
- public void held(final PlayQueueItem item, final View view) {
- if (playQueue.indexOf(item) != -1) {
- openPopupMenu(playQueue, item, view, true,
- getParentActivity().getSupportFragmentManager(), context);
- }
- }
-
- @Override
- public void onStartDrag(final PlayQueueItemHolder viewHolder) {
- if (itemTouchHelper != null) {
- itemTouchHelper.startDrag(viewHolder);
- }
- }
- };
- }
-
@Override // own playback listener
@Nullable
public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) {
@@ -3372,279 +1956,21 @@ public void disablePreloadingOfCurrentTrack() {
@Nullable
public VideoStream getSelectedVideoStream() {
- return (selectedStreamIndex >= 0 && availableStreams != null
- && availableStreams.size() > selectedStreamIndex)
- ? availableStreams.get(selectedStreamIndex) : null;
- }
-
- private void updateStreamRelatedViews() {
- if (!getCurrentStreamInfo().isPresent()) {
- return;
- }
- final StreamInfo info = getCurrentStreamInfo().get();
-
- binding.qualityTextView.setVisibility(View.GONE);
- binding.playbackSpeed.setVisibility(View.GONE);
-
- binding.playbackEndTime.setVisibility(View.GONE);
- binding.playbackLiveSync.setVisibility(View.GONE);
-
- switch (info.getStreamType()) {
- case AUDIO_STREAM:
- case POST_LIVE_AUDIO_STREAM:
- binding.surfaceView.setVisibility(View.GONE);
- binding.endScreen.setVisibility(View.VISIBLE);
- binding.playbackEndTime.setVisibility(View.VISIBLE);
- break;
-
- case AUDIO_LIVE_STREAM:
- binding.surfaceView.setVisibility(View.GONE);
- binding.endScreen.setVisibility(View.VISIBLE);
- binding.playbackLiveSync.setVisibility(View.VISIBLE);
- break;
-
- case LIVE_STREAM:
- binding.surfaceView.setVisibility(View.VISIBLE);
- binding.endScreen.setVisibility(View.GONE);
- binding.playbackLiveSync.setVisibility(View.VISIBLE);
- break;
-
- case VIDEO_STREAM:
- case POST_LIVE_STREAM:
- if (currentMetadata == null
- || !currentMetadata.getMaybeQuality().isPresent()
- || (info.getVideoStreams().isEmpty()
- && info.getVideoOnlyStreams().isEmpty())) {
- break;
- }
-
- availableStreams = currentMetadata.getMaybeQuality().get().getSortedVideoStreams();
- selectedStreamIndex =
- currentMetadata.getMaybeQuality().get().getSelectedVideoStreamIndex();
- buildQualityMenu();
-
- binding.qualityTextView.setVisibility(View.VISIBLE);
- binding.surfaceView.setVisibility(View.VISIBLE);
- default:
- binding.endScreen.setVisibility(View.GONE);
- binding.playbackEndTime.setVisibility(View.VISIBLE);
- break;
- }
-
- buildPlaybackSpeedMenu();
- binding.playbackSpeed.setVisibility(View.VISIBLE);
- }
-
- private void updateQueueTime(final int currentTime) {
- final int currentStream = playQueue.getIndex();
- int before = 0;
- int after = 0;
-
- final List streams = playQueue.getStreams();
- final int nStreams = streams.size();
-
- for (int i = 0; i < nStreams; i++) {
- if (i < currentStream) {
- before += streams.get(i).getDuration();
- } else {
- after += streams.get(i).getDuration();
- }
- }
-
- before *= 1000;
- after *= 1000;
-
- binding.itemsListHeaderDuration.setText(
- String.format("%s/%s",
- getTimeString(currentTime + before),
- getTimeString(before + after)
- ));
- }
- //endregion
-
-
-
- /*//////////////////////////////////////////////////////////////////////////
- // Popup menus ("popup" means that they pop up, not that they belong to the popup player)
- //////////////////////////////////////////////////////////////////////////*/
- //region Popup menus ("popup" means that they pop up, not that they belong to the popup player)
-
- private void buildQualityMenu() {
- if (qualityPopupMenu == null) {
- return;
- }
- qualityPopupMenu.getMenu().removeGroup(POPUP_MENU_ID_QUALITY);
-
- for (int i = 0; i < availableStreams.size(); i++) {
- final VideoStream videoStream = availableStreams.get(i);
- qualityPopupMenu.getMenu().add(POPUP_MENU_ID_QUALITY, i, Menu.NONE, MediaFormat
- .getNameById(videoStream.getFormatId()) + " " + videoStream.getResolution());
- }
- if (getSelectedVideoStream() != null) {
- binding.qualityTextView.setText(getSelectedVideoStream().getResolution());
- }
- qualityPopupMenu.setOnMenuItemClickListener(this);
- qualityPopupMenu.setOnDismissListener(this);
- }
-
- private void buildPlaybackSpeedMenu() {
- if (playbackSpeedPopupMenu == null) {
- return;
- }
- playbackSpeedPopupMenu.getMenu().removeGroup(POPUP_MENU_ID_PLAYBACK_SPEED);
-
- for (int i = 0; i < PLAYBACK_SPEEDS.length; i++) {
- playbackSpeedPopupMenu.getMenu().add(POPUP_MENU_ID_PLAYBACK_SPEED, i, Menu.NONE,
- formatSpeed(PLAYBACK_SPEEDS[i]));
- }
- binding.playbackSpeed.setText(formatSpeed(getPlaybackSpeed()));
- playbackSpeedPopupMenu.setOnMenuItemClickListener(this);
- playbackSpeedPopupMenu.setOnDismissListener(this);
- }
-
- private void buildCaptionMenu(@NonNull final List availableLanguages) {
- if (captionPopupMenu == null) {
- return;
- }
- captionPopupMenu.getMenu().removeGroup(POPUP_MENU_ID_CAPTION);
- captionPopupMenu.setOnDismissListener(this);
-
- // Add option for turning off caption
- final MenuItem captionOffItem = captionPopupMenu.getMenu().add(POPUP_MENU_ID_CAPTION,
- 0, Menu.NONE, R.string.caption_none);
- captionOffItem.setOnMenuItemClickListener(menuItem -> {
- final int textRendererIndex = getCaptionRendererIndex();
- if (textRendererIndex != RENDERER_UNAVAILABLE) {
- trackSelector.setParameters(trackSelector.buildUponParameters()
- .setRendererDisabled(textRendererIndex, true));
- }
- prefs.edit().remove(context.getString(R.string.caption_user_set_key)).apply();
- return true;
- });
-
- // Add all available captions
- for (int i = 0; i < availableLanguages.size(); i++) {
- final String captionLanguage = availableLanguages.get(i);
- final MenuItem captionItem = captionPopupMenu.getMenu().add(POPUP_MENU_ID_CAPTION,
- i + 1, Menu.NONE, captionLanguage);
- captionItem.setOnMenuItemClickListener(menuItem -> {
- final int textRendererIndex = getCaptionRendererIndex();
- if (textRendererIndex != RENDERER_UNAVAILABLE) {
- // DefaultTrackSelector will select for text tracks in the following order.
- // When multiple tracks share the same rank, a random track will be chosen.
- // 1. ANY track exactly matching preferred language name
- // 2. ANY track exactly matching preferred language stem
- // 3. ROLE_FLAG_CAPTION track matching preferred language stem
- // 4. ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND track matching preferred language stem
- // This means if a caption track of preferred language is not available,
- // then an auto-generated track of that language will be chosen automatically.
- trackSelector.setParameters(trackSelector.buildUponParameters()
- .setPreferredTextLanguages(captionLanguage,
- PlayerHelper.captionLanguageStemOf(captionLanguage))
- .setPreferredTextRoleFlags(C.ROLE_FLAG_CAPTION)
- .setRendererDisabled(textRendererIndex, false));
- prefs.edit().putString(context.getString(R.string.caption_user_set_key),
- captionLanguage).apply();
- }
- return true;
- });
- }
-
- // apply caption language from previous user preference
- final int textRendererIndex = getCaptionRendererIndex();
- if (textRendererIndex == RENDERER_UNAVAILABLE) {
- return;
- }
-
- // If user prefers to show no caption, then disable the renderer.
- // Otherwise, DefaultTrackSelector may automatically find an available caption
- // and display that.
- final String userPreferredLanguage =
- prefs.getString(context.getString(R.string.caption_user_set_key), null);
- if (userPreferredLanguage == null) {
- trackSelector.setParameters(trackSelector.buildUponParameters()
- .setRendererDisabled(textRendererIndex, true));
- return;
- }
-
- // Only set preferred language if it does not match the user preference,
- // otherwise there might be an infinite cycle at onTextTracksChanged.
- final List selectedPreferredLanguages =
- trackSelector.getParameters().preferredTextLanguages;
- if (!selectedPreferredLanguages.contains(userPreferredLanguage)) {
- trackSelector.setParameters(trackSelector.buildUponParameters()
- .setPreferredTextLanguages(userPreferredLanguage,
- PlayerHelper.captionLanguageStemOf(userPreferredLanguage))
- .setPreferredTextRoleFlags(C.ROLE_FLAG_CAPTION)
- .setRendererDisabled(textRendererIndex, false));
- }
- }
-
- /**
- * Called when an item of the quality selector or the playback speed selector is selected.
- */
- @Override
- public boolean onMenuItemClick(@NonNull final MenuItem menuItem) {
- if (DEBUG) {
- Log.d(TAG, "onMenuItemClick() called with: "
- + "menuItem = [" + menuItem + "], "
- + "menuItem.getItemId = [" + menuItem.getItemId() + "]");
- }
-
- if (menuItem.getGroupId() == POPUP_MENU_ID_QUALITY) {
- final int menuItemIndex = menuItem.getItemId();
- if (selectedStreamIndex == menuItemIndex || availableStreams == null
- || availableStreams.size() <= menuItemIndex) {
- return true;
- }
-
- saveStreamProgressState(); //TODO added, check if good
- final String newResolution = availableStreams.get(menuItemIndex).getResolution();
- setRecovery();
- setPlaybackQuality(newResolution);
- reloadPlayQueueManager();
-
- binding.qualityTextView.setText(menuItem.getTitle());
- return true;
- } else if (menuItem.getGroupId() == POPUP_MENU_ID_PLAYBACK_SPEED) {
- final int speedIndex = menuItem.getItemId();
- final float speed = PLAYBACK_SPEEDS[speedIndex];
-
- setPlaybackSpeed(speed);
- binding.playbackSpeed.setText(formatSpeed(speed));
+ @Nullable final MediaItemTag.Quality quality = Optional.ofNullable(currentMetadata)
+ .flatMap(MediaItemTag::getMaybeQuality)
+ .orElse(null);
+ if (quality == null) {
+ return null;
}
- return false;
- }
-
- /**
- * Called when some popup menu is dismissed.
- */
- @Override
- public void onDismiss(@Nullable final PopupMenu menu) {
- if (DEBUG) {
- Log.d(TAG, "onDismiss() called with: menu = [" + menu + "]");
- }
- isSomePopupMenuVisible = false; //TODO check if this works
- if (getSelectedVideoStream() != null) {
- binding.qualityTextView.setText(getSelectedVideoStream().getResolution());
- }
- if (isPlaying()) {
- hideControls(DEFAULT_CONTROLS_DURATION, 0);
- hideSystemUIIfNeeded();
- }
- }
+ final List availableStreams = quality.getSortedVideoStreams();
+ final int selectedStreamIndex = quality.getSelectedVideoStreamIndex();
- private void onCaptionClicked() {
- if (DEBUG) {
- Log.d(TAG, "onCaptionClicked() called");
+ if (selectedStreamIndex >= 0 && availableStreams.size() > selectedStreamIndex) {
+ return availableStreams.get(selectedStreamIndex);
+ } else {
+ return null;
}
- captionPopupMenu.show();
- isSomePopupMenuVisible = true;
- }
-
- private void setPlaybackQuality(@Nullable final String quality) {
- videoResolver.setPlaybackQuality(quality);
}
//endregion
@@ -3655,68 +1981,7 @@ private void setPlaybackQuality(@Nullable final String quality) {
//////////////////////////////////////////////////////////////////////////*/
//region Captions (text tracks)
- private void setupSubtitleView() {
- final float captionScale = PlayerHelper.getCaptionScale(context);
- final CaptionStyleCompat captionStyle = PlayerHelper.getCaptionStyle(context);
- if (popupPlayerSelected()) {
- final float captionRatio = (captionScale - 1.0f) / 5.0f + 1.0f;
- binding.subtitleView.setFractionalTextSize(
- SubtitleView.DEFAULT_TEXT_SIZE_FRACTION * captionRatio);
- } else {
- final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
- final int minimumLength = Math.min(metrics.heightPixels, metrics.widthPixels);
- final float captionRatioInverse = 20f + 4f * (1.0f - captionScale);
- binding.subtitleView.setFixedTextSize(
- TypedValue.COMPLEX_UNIT_PX, minimumLength / captionRatioInverse);
- }
- binding.subtitleView.setApplyEmbeddedStyles(captionStyle == CaptionStyleCompat.DEFAULT);
- binding.subtitleView.setStyle(captionStyle);
- }
-
- private void onTextTracksChanged(@NonNull final Tracks currentTrack) {
- if (binding == null) {
- return;
- }
-
- final boolean trackTypeTextSupported = !currentTrack.containsType(C.TRACK_TYPE_TEXT)
- || currentTrack.isTypeSupported(C.TRACK_TYPE_TEXT, false);
- if (trackSelector.getCurrentMappedTrackInfo() == null || !trackTypeTextSupported) {
- binding.captionTextView.setVisibility(View.GONE);
- return;
- }
-
- // Extract all loaded languages
- final List textTracks = currentTrack
- .getGroups()
- .stream()
- .filter(trackGroupInfo -> C.TRACK_TYPE_TEXT == trackGroupInfo.getType())
- .collect(Collectors.toList());
- final List availableLanguages = textTracks.stream()
- .map(Tracks.Group::getMediaTrackGroup)
- .filter(textTrack -> textTrack.length > 0)
- .map(textTrack -> textTrack.getFormat(0).language)
- .collect(Collectors.toList());
-
- // Find selected text track
- final Optional selectedTracks = textTracks.stream()
- .filter(Tracks.Group::isSelected)
- .filter(info -> info.getMediaTrackGroup().length >= 1)
- .map(info -> info.getMediaTrackGroup().getFormat(0))
- .findFirst();
-
- // Build UI
- buildCaptionMenu(availableLanguages);
- if (trackSelector.getParameters().getRendererDisabled(getCaptionRendererIndex())
- || !selectedTracks.isPresent()) {
- binding.captionTextView.setText(R.string.caption_none);
- } else {
- binding.captionTextView.setText(selectedTracks.get().language);
- }
- binding.captionTextView.setVisibility(
- availableLanguages.isEmpty() ? View.GONE : View.VISIBLE);
- }
-
- private int getCaptionRendererIndex() {
+ public int getCaptionRendererIndex() {
if (exoPlayerIsNull()) {
return RENDERER_UNAVAILABLE;
}
@@ -3732,218 +1997,10 @@ private int getCaptionRendererIndex() {
//endregion
-
- /*//////////////////////////////////////////////////////////////////////////
- // Click listeners
- //////////////////////////////////////////////////////////////////////////*/
- //region Click listeners
-
- @Override
- public void onClick(final View v) {
- if (DEBUG) {
- Log.d(TAG, "onClick() called with: v = [" + v + "]");
- }
- if (v.getId() == binding.resizeTextView.getId()) {
- onResizeClicked();
- } else if (v.getId() == binding.captionTextView.getId()) {
- onCaptionClicked();
- } else if (v.getId() == binding.playbackLiveSync.getId()) {
- seekToDefault();
- } else if (v.getId() == binding.playPauseButton.getId()) {
- playPause();
- } else if (v.getId() == binding.playPreviousButton.getId()) {
- playPrevious();
- } else if (v.getId() == binding.playNextButton.getId()) {
- playNext();
- } else if (v.getId() == binding.moreOptionsButton.getId()) {
- onMoreOptionsClicked();
- } else if (v.getId() == binding.share.getId()) {
- ShareUtils.shareText(context, getVideoTitle(), getVideoUrlAtCurrentTime(),
- currentItem.getThumbnailUrl());
- } else if (v.getId() == binding.playWithKodi.getId()) {
- onPlayWithKodiClicked();
- } else if (v.getId() == binding.openInBrowser.getId()) {
- onOpenInBrowserClicked();
- } else if (v.getId() == binding.fullScreenButton.getId()) {
- setRecovery();
- NavigationHelper.playOnMainPlayer(context, playQueue, true);
- return;
- } else if (v.getId() == binding.screenRotationButton.getId()) {
- // Only if it's not a vertical video or vertical video but in landscape with locked
- // orientation a screen orientation can be changed automatically
- if (!isVerticalVideo
- || (service.isLandscape() && globalScreenOrientationLocked(context))) {
- fragmentListener.onScreenRotationButtonClicked();
- } else {
- toggleFullscreen();
- }
- } else if (v.getId() == binding.switchMute.getId()) {
- onMuteUnmuteButtonClicked();
- } else if (v.getId() == binding.playerCloseButton.getId()) {
- context.sendBroadcast(new Intent(VideoDetailFragment.ACTION_HIDE_MAIN_PLAYER));
- }
-
- manageControlsAfterOnClick(v);
- }
-
- /**
- * Manages the controls after a click occurred on the player UI.
- * @param v – The view that was clicked
- */
- public void manageControlsAfterOnClick(@NonNull final View v) {
- if (currentState == STATE_COMPLETED) {
- return;
- }
-
- controlsVisibilityHandler.removeCallbacksAndMessages(null);
- showHideShadow(true, DEFAULT_CONTROLS_DURATION);
- animate(binding.playbackControlRoot, true, DEFAULT_CONTROLS_DURATION,
- AnimationType.ALPHA, 0, () -> {
- if (currentState == STATE_PLAYING && !isSomePopupMenuVisible) {
- if (v.getId() == binding.playPauseButton.getId()
- // Hide controls in fullscreen immediately
- || (v.getId() == binding.screenRotationButton.getId()
- && isFullscreen)) {
- hideControls(0, 0);
- } else {
- hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
- }
- }
- });
- }
-
- @Override
- public boolean onLongClick(final View v) {
- if (v.getId() == binding.moreOptionsButton.getId() && isFullscreen) {
- fragmentListener.onMoreOptionsLongClicked();
- hideControls(0, 0);
- hideSystemUIIfNeeded();
- } else if (v.getId() == binding.share.getId()) {
- ShareUtils.copyToClipboard(context, getVideoUrlAtCurrentTime());
- }
- return true;
- }
-
- public boolean onKeyDown(final int keyCode) {
- switch (keyCode) {
- default:
- break;
- case KeyEvent.KEYCODE_SPACE:
- if (isFullscreen) {
- playPause();
- if (isPlaying()) {
- hideControls(0, 0);
- }
- return true;
- }
- break;
- case KeyEvent.KEYCODE_BACK:
- if (DeviceUtils.isTv(context) && isControlsVisible()) {
- hideControls(0, 0);
- return true;
- }
- break;
- case KeyEvent.KEYCODE_DPAD_UP:
- case KeyEvent.KEYCODE_DPAD_LEFT:
- case KeyEvent.KEYCODE_DPAD_DOWN:
- case KeyEvent.KEYCODE_DPAD_RIGHT:
- case KeyEvent.KEYCODE_DPAD_CENTER:
- if ((binding.getRoot().hasFocus() && !binding.playbackControlRoot.hasFocus())
- || isQueueVisible) {
- // do not interfere with focus in playlist and play queue etc.
- return false;
- }
-
- if (currentState == Player.STATE_BLOCKED) {
- return true;
- }
-
- if (isControlsVisible()) {
- hideControls(DEFAULT_CONTROLS_DURATION, DPAD_CONTROLS_HIDE_TIME);
- } else {
- binding.playPauseButton.requestFocus();
- showControlsThenHide();
- showSystemUIPartially();
- return true;
- }
- break;
- }
-
- return false;
- }
-
- private void onMoreOptionsClicked() {
- if (DEBUG) {
- Log.d(TAG, "onMoreOptionsClicked() called");
- }
-
- final boolean isMoreControlsVisible =
- binding.secondaryControls.getVisibility() == View.VISIBLE;
-
- animateRotation(binding.moreOptionsButton, DEFAULT_CONTROLS_DURATION,
- isMoreControlsVisible ? 0 : 180);
- animate(binding.secondaryControls, !isMoreControlsVisible, DEFAULT_CONTROLS_DURATION,
- AnimationType.SLIDE_AND_ALPHA, 0, () -> {
- // Fix for a ripple effect on background drawable.
- // When view returns from GONE state it takes more milliseconds than returning
- // from INVISIBLE state. And the delay makes ripple background end to fast
- if (isMoreControlsVisible) {
- binding.secondaryControls.setVisibility(View.INVISIBLE);
- }
- });
- showControls(DEFAULT_CONTROLS_DURATION);
- }
-
- private void onPlayWithKodiClicked() {
- if (currentMetadata != null) {
- pause();
- try {
- NavigationHelper.playWithKore(context, Uri.parse(getVideoUrl()));
- } catch (final Exception e) {
- if (DEBUG) {
- Log.i(TAG, "Failed to start kore", e);
- }
- KoreUtils.showInstallKoreDialog(getParentActivity());
- }
- }
- }
-
- private void onOpenInBrowserClicked() {
- getCurrentStreamInfo()
- .map(Info::getOriginalUrl)
- .ifPresent(originalUrl -> ShareUtils.openUrlInBrowser(
- Objects.requireNonNull(getParentActivity()), originalUrl));
- }
- //endregion
-
-
-
/*//////////////////////////////////////////////////////////////////////////
// Video size, resize, orientation, fullscreen
//////////////////////////////////////////////////////////////////////////*/
//region Video size, resize, orientation, fullscreen
-
- private void setupScreenRotationButton() {
- binding.screenRotationButton.setVisibility(videoPlayerSelected()
- && (globalScreenOrientationLocked(context) || isVerticalVideo
- || DeviceUtils.isTablet(context))
- ? View.VISIBLE : View.GONE);
- binding.screenRotationButton.setImageDrawable(AppCompatResources.getDrawable(context,
- isFullscreen ? R.drawable.ic_fullscreen_exit
- : R.drawable.ic_fullscreen));
- }
-
- private void setResizeMode(@AspectRatioFrameLayout.ResizeMode final int resizeMode) {
- binding.surfaceView.setResizeMode(resizeMode);
- binding.resizeTextView.setText(PlayerHelper.resizeTypeOf(context, resizeMode));
- }
-
- void onResizeClicked() {
- if (binding != null) {
- setResizeMode(nextResizeModeAndSaveToPrefs(this, binding.surfaceView.getResizeMode()));
- }
- }
-
@Override // exoplayer listener
public void onVideoSizeChanged(@NonNull final VideoSize videoSize) {
if (DEBUG) {
@@ -3954,137 +2011,11 @@ public void onVideoSizeChanged(@NonNull final VideoSize videoSize) {
+ "pixelWidthHeightRatio = [" + videoSize.pixelWidthHeightRatio + "]");
}
- binding.surfaceView.setAspectRatio(((float) videoSize.width) / videoSize.height);
- isVerticalVideo = videoSize.width < videoSize.height;
-
- if (globalScreenOrientationLocked(context)
- && isFullscreen
- && service.isLandscape() == isVerticalVideo
- && !DeviceUtils.isTv(context)
- && !DeviceUtils.isTablet(context)
- && fragmentListener != null) {
- // set correct orientation
- fragmentListener.onScreenRotationButtonClicked();
- }
-
- setupScreenRotationButton();
- }
-
- public void toggleFullscreen() {
- if (DEBUG) {
- Log.d(TAG, "toggleFullscreen() called");
- }
- if (popupPlayerSelected() || exoPlayerIsNull() || fragmentListener == null) {
- return;
- }
-
- isFullscreen = !isFullscreen;
- if (!isFullscreen) {
- // Apply window insets because Android will not do it when orientation changes
- // from landscape to portrait (open vertical video to reproduce)
- binding.playbackControlRoot.setPadding(0, 0, 0, 0);
- } else {
- // Android needs tens milliseconds to send new insets but a user is able to see
- // how controls changes it's position from `0` to `nav bar height` padding.
- // So just hide the controls to hide this visual inconsistency
- hideControls(0, 0);
- }
- fragmentListener.onFullscreenStateChanged(isFullscreen);
-
- if (isFullscreen) {
- binding.titleTextView.setVisibility(View.VISIBLE);
- binding.channelTextView.setVisibility(View.VISIBLE);
- binding.playerCloseButton.setVisibility(View.GONE);
- } else {
- binding.titleTextView.setVisibility(View.GONE);
- binding.channelTextView.setVisibility(View.GONE);
- binding.playerCloseButton.setVisibility(
- videoPlayerSelected() ? View.VISIBLE : View.GONE);
- }
- setupScreenRotationButton();
- }
-
- public void checkLandscape() {
- final AppCompatActivity parent = getParentActivity();
- final boolean videoInLandscapeButNotInFullscreen =
- service.isLandscape() && !isFullscreen && videoPlayerSelected() && !isAudioOnly;
-
- final boolean notPaused = currentState != STATE_COMPLETED && currentState != STATE_PAUSED;
- if (parent != null
- && videoInLandscapeButNotInFullscreen
- && notPaused
- && !DeviceUtils.isTablet(context)) {
- toggleFullscreen();
- }
- }
- //endregion
-
-
-
- /*//////////////////////////////////////////////////////////////////////////
- // Gestures
- //////////////////////////////////////////////////////////////////////////*/
- //region Gestures
-
- @SuppressWarnings("checkstyle:ParameterNumber")
- private void onLayoutChange(final View view, final int l, final int t, final int r, final int b,
- final int ol, final int ot, final int or, final int ob) {
- if (l != ol || t != ot || r != or || b != ob) {
- // Use smaller value to be consistent between screen orientations
- // (and to make usage easier)
- final int width = r - l;
- final int height = b - t;
- final int min = Math.min(width, height);
- maxGestureLength = (int) (min * MAX_GESTURE_LENGTH);
-
- if (DEBUG) {
- Log.d(TAG, "maxGestureLength = " + maxGestureLength);
- }
-
- binding.volumeProgressBar.setMax(maxGestureLength);
- binding.brightnessProgressBar.setMax(maxGestureLength);
-
- setInitialGestureValues();
- binding.itemsListPanel.getLayoutParams().height
- = height - binding.itemsListPanel.getTop();
- }
- }
-
- private void setInitialGestureValues() {
- if (audioReactor != null) {
- final float currentVolumeNormalized =
- (float) audioReactor.getVolume() / audioReactor.getMaxVolume();
- binding.volumeProgressBar.setProgress(
- (int) (binding.volumeProgressBar.getMax() * currentVolumeNormalized));
- }
- }
-
- private int distanceFromCloseButton(@NonNull final MotionEvent popupMotionEvent) {
- final int closeOverlayButtonX = closeOverlayBinding.closeButton.getLeft()
- + closeOverlayBinding.closeButton.getWidth() / 2;
- final int closeOverlayButtonY = closeOverlayBinding.closeButton.getTop()
- + closeOverlayBinding.closeButton.getHeight() / 2;
-
- final float fingerX = popupLayoutParams.x + popupMotionEvent.getX();
- final float fingerY = popupLayoutParams.y + popupMotionEvent.getY();
-
- return (int) Math.sqrt(Math.pow(closeOverlayButtonX - fingerX, 2)
- + Math.pow(closeOverlayButtonY - fingerY, 2));
- }
-
- private float getClosingRadius() {
- final int buttonRadius = closeOverlayBinding.closeButton.getWidth() / 2;
- // 20% wider than the button itself
- return buttonRadius * 1.2f;
- }
-
- public boolean isInsideClosingRadius(@NonNull final MotionEvent popupMotionEvent) {
- return distanceFromCloseButton(popupMotionEvent) <= getClosingRadius();
+ UIs.call(playerUi -> playerUi.onVideoSizeChanged(videoSize));
}
//endregion
-
/*//////////////////////////////////////////////////////////////////////////
// Activity / fragment binding
//////////////////////////////////////////////////////////////////////////*/
@@ -4092,13 +2023,7 @@ public boolean isInsideClosingRadius(@NonNull final MotionEvent popupMotionEvent
public void setFragmentListener(final PlayerServiceEventListener listener) {
fragmentListener = listener;
- fragmentIsVisible = true;
- // Apply window insets because Android will not do it when orientation changes
- // from landscape to portrait
- if (!isFullscreen) {
- binding.playbackControlRoot.setPadding(0, 0, 0, 0);
- }
- binding.itemsListPanel.setPadding(0, 0, 0, 0);
+ UIs.call(PlayerUi::onFragmentListenerSet);
notifyQueueUpdateToListeners();
notifyMetadataUpdateToListeners();
notifyPlaybackUpdateToListeners();
@@ -4136,28 +2061,6 @@ void stopActivityBinding() {
}
}
- /**
- * This will be called when a user goes to another app/activity, turns off a screen.
- * We don't want to interrupt playback and don't want to see notification so
- * next lines of code will enable audio-only playback only if needed
- */
- private void onFragmentStopped() {
- if (videoPlayerSelected() && (isPlaying() || isLoading())) {
- switch (getMinimizeOnExitAction(context)) {
- case MINIMIZE_ON_EXIT_MODE_BACKGROUND:
- useVideoSource(false);
- break;
- case MINIMIZE_ON_EXIT_MODE_POPUP:
- setRecovery();
- NavigationHelper.playOnPopupPlayer(getParentActivity(), playQueue, true);
- break;
- case MINIMIZE_ON_EXIT_MODE_NONE: default:
- pause();
- break;
- }
- }
- }
-
private void notifyQueueUpdateToListeners() {
if (fragmentListener != null && playQueue != null) {
fragmentListener.onQueueUpdate(playQueue);
@@ -4200,27 +2103,12 @@ private void notifyProgressUpdateToListeners(final int currentProgress,
}
}
- @Nullable
- public AppCompatActivity getParentActivity() {
- // ! instanceof ViewGroup means that view was added via windowManager for Popup
- if (binding == null || !(binding.getRoot().getParent() instanceof ViewGroup)) {
- return null;
- }
-
- return (AppCompatActivity) ((ViewGroup) binding.getRoot().getParent()).getContext();
- }
-
- private void useVideoSource(final boolean videoEnabled) {
+ public void useVideoSource(final boolean videoEnabled) {
if (playQueue == null || isAudioOnly == !videoEnabled || audioPlayerSelected()) {
return;
}
isAudioOnly = !videoEnabled;
- // When a user returns from background, controls could be hidden but SystemUI will be shown
- // 100%. Hide it.
- if (!isAudioOnly && !isControlsVisible()) {
- hideSystemUIIfNeeded();
- }
// The current metadata may be null sometimes (for e.g. when using an unstable connection
// in livestreams) so we will be not able to execute the block below.
@@ -4332,7 +2220,7 @@ && isNullOrEmpty(streamInfo.getAudioStreams()))) {
//////////////////////////////////////////////////////////////////////////*/
//region Getters
- private Optional getCurrentStreamInfo() {
+ public Optional getCurrentStreamInfo() {
return Optional.ofNullable(currentMetadata).flatMap(MediaItemTag::getMaybeStreamInfo);
}
@@ -4344,6 +2232,10 @@ public boolean exoPlayerIsNull() {
return simpleExoPlayer == null;
}
+ public ExoPlayer getExoPlayer() {
+ return simpleExoPlayer;
+ }
+
public boolean isStopped() {
return exoPlayerIsNull() || simpleExoPlayer.getPlaybackState() == ExoPlayer.STATE_IDLE;
}
@@ -4356,7 +2248,7 @@ public boolean getPlayWhenReady() {
return !exoPlayerIsNull() && simpleExoPlayer.getPlayWhenReady();
}
- private boolean isLoading() {
+ public boolean isLoading() {
return !exoPlayerIsNull() && simpleExoPlayer.isLoading();
}
@@ -4372,6 +2264,10 @@ private boolean isLive() {
}
}
+ public void setPlaybackQuality(@Nullable final String quality) {
+ videoResolver.setPlaybackQuality(quality);
+ }
+
@NonNull
public Context getContext() {
@@ -4397,7 +2293,7 @@ public boolean audioPlayerSelected() {
}
public boolean videoPlayerSelected() {
- return playerType == PlayerType.VIDEO;
+ return playerType == PlayerType.MAIN;
}
public boolean popupPlayerSelected() {
@@ -4414,156 +2310,39 @@ public AudioReactor getAudioReactor() {
return audioReactor;
}
- public GestureDetector getGestureDetector() {
- return gestureDetector;
+ public PlayerService getService() {
+ return service;
}
- public boolean isFullscreen() {
- return isFullscreen;
+ public boolean isAudioOnly() {
+ return isAudioOnly;
}
- public boolean isVerticalVideo() {
- return isVerticalVideo;
- }
-
- public boolean isPopupClosing() {
- return isPopupClosing;
- }
-
-
- public boolean isSomePopupMenuVisible() {
- return isSomePopupMenuVisible;
- }
-
- public void setSomePopupMenuVisible(final boolean somePopupMenuVisible) {
- isSomePopupMenuVisible = somePopupMenuVisible;
- }
-
- public ImageButton getPlayPauseButton() {
- return binding.playPauseButton;
- }
-
- public View getClosingOverlayView() {
- return binding.closingOverlay;
- }
-
- public ProgressBar getVolumeProgressBar() {
- return binding.volumeProgressBar;
- }
-
- public ProgressBar getBrightnessProgressBar() {
- return binding.brightnessProgressBar;
- }
-
- public int getMaxGestureLength() {
- return maxGestureLength;
- }
-
- public ImageView getVolumeImageView() {
- return binding.volumeImageView;
- }
-
- public RelativeLayout getVolumeRelativeLayout() {
- return binding.volumeRelativeLayout;
- }
-
- public ImageView getBrightnessImageView() {
- return binding.brightnessImageView;
- }
-
- public RelativeLayout getBrightnessRelativeLayout() {
- return binding.brightnessRelativeLayout;
- }
-
- public FloatingActionButton getCloseOverlayButton() {
- return closeOverlayBinding.closeButton;
- }
-
- public View getLoadingPanel() {
- return binding.loadingPanel;
- }
-
- public TextView getCurrentDisplaySeek() {
- return binding.currentDisplaySeek;
- }
-
- public PlayerFastSeekOverlay getFastSeekOverlay() {
- return binding.fastSeekOverlay;
+ @NonNull
+ public DefaultTrackSelector getTrackSelector() {
+ return trackSelector;
}
@Nullable
- public WindowManager.LayoutParams getPopupLayoutParams() {
- return popupLayoutParams;
+ public MediaItemTag getCurrentMetadata() {
+ return currentMetadata;
}
@Nullable
- public WindowManager getWindowManager() {
- return windowManager;
- }
-
- public float getScreenWidth() {
- return screenWidth;
+ public PlayQueueItem getCurrentItem() {
+ return currentItem;
}
- public float getScreenHeight() {
- return screenHeight;
+ public Optional getFragmentListener() {
+ return Optional.ofNullable(fragmentListener);
}
- public View getRootView() {
- return binding.getRoot();
- }
-
- public ExpandableSurfaceView getSurfaceView() {
- return binding.surfaceView;
- }
-
- public PlayQueueAdapter getPlayQueueAdapter() {
- return playQueueAdapter;
- }
-
- public PlayerBinding getBinding() {
- return binding;
- }
-
- //endregion
-
-
- /*//////////////////////////////////////////////////////////////////////////
- // SurfaceHolderCallback helpers
- //////////////////////////////////////////////////////////////////////////*/
- //region SurfaceHolderCallback helpers
-
- private void setupVideoSurface() {
- // make sure there is nothing left over from previous calls
- cleanupVideoSurface();
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // >=API23
- surfaceHolderCallback = new SurfaceHolderCallback(context, simpleExoPlayer);
- binding.surfaceView.getHolder().addCallback(surfaceHolderCallback);
- final Surface surface = binding.surfaceView.getHolder().getSurface();
- // ensure player is using an unreleased surface, which the surfaceView might not be
- // when starting playback on background or during player switching
- if (surface.isValid()) {
- // initially set the surface manually otherwise
- // onRenderedFirstFrame() will not be called
- simpleExoPlayer.setVideoSurface(surface);
- }
- } else {
- simpleExoPlayer.setVideoSurfaceView(binding.surfaceView);
- }
- }
-
- private void cleanupVideoSurface() {
- // Only for API >= 23
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && surfaceHolderCallback != null) {
- if (binding != null) {
- binding.surfaceView.getHolder().removeCallback(surfaceHolderCallback);
- }
- surfaceHolderCallback.release();
- surfaceHolderCallback = null;
- }
+ /**
+ * @return the user interfaces connected with the player
+ */
+ public PlayerUiList UIs() {
+ return UIs;
}
- //endregion
/**
* Get the video renderer index of the current playing stream.
@@ -4592,4 +2371,5 @@ private int getVideoRendererIndex() {
// No video renderer index with at least one track found: return unavailable index
.orElse(RENDERER_UNAVAILABLE);
}
+ //endregion
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java b/app/src/main/java/org/schabi/newpipe/player/PlayerService.java
similarity index 63%
rename from app/src/main/java/org/schabi/newpipe/player/MainPlayer.java
rename to app/src/main/java/org/schabi/newpipe/player/PlayerService.java
index a9b9f4c8762..cf83dc5c277 100644
--- a/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java
+++ b/app/src/main/java/org/schabi/newpipe/player/PlayerService.java
@@ -19,44 +19,35 @@
package org.schabi.newpipe.player;
+import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
+
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.WindowManager;
-
-import androidx.annotation.Nullable;
-import androidx.core.content.ContextCompat;
import org.schabi.newpipe.App;
-import org.schabi.newpipe.databinding.PlayerBinding;
-import org.schabi.newpipe.util.DeviceUtils;
+import org.schabi.newpipe.player.ui.VideoPlayerUi;
import org.schabi.newpipe.util.ThemeHelper;
-import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
-
/**
* One service for all players.
*
* @author mauriciocolli
*/
-public final class MainPlayer extends Service {
- private static final String TAG = "MainPlayer";
+public final class PlayerService extends Service {
+ private static final String TAG = PlayerService.class.getSimpleName();
private static final boolean DEBUG = Player.DEBUG;
private Player player;
- private WindowManager windowManager;
- private final IBinder mBinder = new MainPlayer.LocalBinder();
+ private final IBinder mBinder = new PlayerService.LocalBinder();
public enum PlayerType {
- VIDEO,
+ MAIN,
AUDIO,
POPUP
}
@@ -67,7 +58,7 @@ public enum PlayerType {
static final String ACTION_CLOSE
= App.PACKAGE_NAME + ".player.MainPlayer.CLOSE";
- static final String ACTION_PLAY_PAUSE
+ public static final String ACTION_PLAY_PAUSE
= App.PACKAGE_NAME + ".player.MainPlayer.PLAY_PAUSE";
static final String ACTION_REPEAT
= App.PACKAGE_NAME + ".player.MainPlayer.REPEAT";
@@ -94,19 +85,12 @@ public void onCreate() {
Log.d(TAG, "onCreate() called");
}
assureCorrectAppLanguage(this);
- windowManager = ContextCompat.getSystemService(this, WindowManager.class);
-
ThemeHelper.setTheme(this);
- createView();
- }
-
- private void createView() {
- final PlayerBinding binding = PlayerBinding.inflate(LayoutInflater.from(this));
player = new Player(this);
- player.setupFromView(binding);
-
- NotificationUtil.getInstance().createNotificationAndStartForeground(player, this);
+ /*final MainPlayerUi mainPlayerUi = new MainPlayerUi(player,
+ PlayerBinding.inflate(LayoutInflater.from(this)));
+ player.UIs().add(mainPlayerUi);*/
}
@Override
@@ -121,11 +105,6 @@ public int onStartCommand(final Intent intent, final int flags, final int startI
return START_NOT_STICKY;
}
- if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
- || intent.getStringExtra(Player.PLAY_QUEUE_KEY) != null) {
- NotificationUtil.getInstance().createNotificationAndStartForeground(player, this);
- }
-
player.handleIntent(intent);
if (player.getMediaSessionManager() != null) {
player.getMediaSessionManager().handleMediaButtonIntent(intent);
@@ -144,13 +123,7 @@ public void stopForImmediateReusing() {
// Releases wifi & cpu, disables keepScreenOn, etc.
// We can't just pause the player here because it will make transition
// from one stream to a new stream not smooth
- player.smoothStopPlayer();
- player.setRecovery();
-
- // Android TV will handle back button in case controls will be visible
- // (one more additional unneeded click while the player is hidden)
- player.hideControls(0, 0);
- player.closeItemsList();
+ player.smoothStopForImmediateReusing();
// Notification shows information about old stream but if a user selects
// a stream from backStack it's not actual anymore
@@ -180,18 +153,7 @@ public void onDestroy() {
private void cleanup() {
if (player != null) {
- // Exit from fullscreen when user closes the player via notification
- if (player.isFullscreen()) {
- player.toggleFullscreen();
- }
- removeViewFromParent();
-
- player.saveStreamProgressState();
- player.setRecovery();
- player.stopActivityBinding();
- player.removePopupFromView();
player.destroy();
-
player = null;
}
}
@@ -212,48 +174,14 @@ public IBinder onBind(final Intent intent) {
return mBinder;
}
- /*//////////////////////////////////////////////////////////////////////////
- // Utils
- //////////////////////////////////////////////////////////////////////////*/
-
- boolean isLandscape() {
- // DisplayMetrics from activity context knows about MultiWindow feature
- // while DisplayMetrics from app context doesn't
- return DeviceUtils.isLandscape(player != null && player.getParentActivity() != null
- ? player.getParentActivity() : this);
- }
-
- @Nullable
- public View getView() {
- if (player == null) {
- return null;
- }
-
- return player.getRootView();
- }
-
- public void removeViewFromParent() {
- if (getView() != null && getView().getParent() != null) {
- if (player.getParentActivity() != null) {
- // This means view was added to fragment
- final ViewGroup parent = (ViewGroup) getView().getParent();
- parent.removeView(getView());
- } else {
- // This means view was added by windowManager for popup player
- windowManager.removeViewImmediate(getView());
- }
- }
- }
-
-
public class LocalBinder extends Binder {
- public MainPlayer getService() {
- return MainPlayer.this;
+ public PlayerService getService() {
+ return PlayerService.this;
}
public Player getPlayer() {
- return MainPlayer.this.player;
+ return PlayerService.this.player;
}
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/event/BasePlayerGestureListener.kt b/app/src/main/java/org/schabi/newpipe/player/event/BasePlayerGestureListener.kt
deleted file mode 100644
index c89eabb4785..00000000000
--- a/app/src/main/java/org/schabi/newpipe/player/event/BasePlayerGestureListener.kt
+++ /dev/null
@@ -1,520 +0,0 @@
-package org.schabi.newpipe.player.event
-
-import android.content.Context
-import android.os.Handler
-import android.util.Log
-import android.view.GestureDetector
-import android.view.MotionEvent
-import android.view.View
-import android.view.ViewConfiguration
-import org.schabi.newpipe.ktx.animate
-import org.schabi.newpipe.player.MainPlayer
-import org.schabi.newpipe.player.Player
-import org.schabi.newpipe.player.helper.PlayerHelper
-import org.schabi.newpipe.player.helper.PlayerHelper.savePopupPositionAndSizeToPrefs
-import kotlin.math.abs
-import kotlin.math.hypot
-import kotlin.math.max
-import kotlin.math.min
-
-/**
- * Base gesture handling for [Player]
- *
- * This class contains the logic for the player gestures like View preparations
- * and provides some abstract methods to make it easier separating the logic from the UI.
- */
-abstract class BasePlayerGestureListener(
- @JvmField
- protected val player: Player,
- @JvmField
- protected val service: MainPlayer
-) : GestureDetector.SimpleOnGestureListener(), View.OnTouchListener {
-
- // ///////////////////////////////////////////////////////////////////
- // Abstract methods for VIDEO and POPUP
- // ///////////////////////////////////////////////////////////////////
-
- abstract fun onDoubleTap(event: MotionEvent, portion: DisplayPortion)
-
- abstract fun onSingleTap(playerType: MainPlayer.PlayerType)
-
- abstract fun onScroll(
- playerType: MainPlayer.PlayerType,
- portion: DisplayPortion,
- initialEvent: MotionEvent,
- movingEvent: MotionEvent,
- distanceX: Float,
- distanceY: Float
- )
-
- abstract fun onScrollEnd(playerType: MainPlayer.PlayerType, event: MotionEvent)
-
- // ///////////////////////////////////////////////////////////////////
- // Abstract methods for POPUP (exclusive)
- // ///////////////////////////////////////////////////////////////////
-
- abstract fun onPopupResizingStart()
-
- abstract fun onPopupResizingEnd()
-
- private var initialPopupX: Int = -1
- private var initialPopupY: Int = -1
-
- private var isMovingInMain = false
- private var isMovingInPopup = false
- private var isResizing = false
-
- private val tossFlingVelocity = PlayerHelper.getTossFlingVelocity()
-
- // [popup] initial coordinates and distance between fingers
- private var initPointerDistance = -1.0
- private var initFirstPointerX = -1f
- private var initFirstPointerY = -1f
- private var initSecPointerX = -1f
- private var initSecPointerY = -1f
-
- // ///////////////////////////////////////////////////////////////////
- // onTouch implementation
- // ///////////////////////////////////////////////////////////////////
-
- override fun onTouch(v: View, event: MotionEvent): Boolean {
- return if (player.popupPlayerSelected()) {
- onTouchInPopup(v, event)
- } else {
- onTouchInMain(v, event)
- }
- }
-
- private fun onTouchInMain(v: View, event: MotionEvent): Boolean {
- player.gestureDetector.onTouchEvent(event)
- if (event.action == MotionEvent.ACTION_UP && isMovingInMain) {
- isMovingInMain = false
- onScrollEnd(MainPlayer.PlayerType.VIDEO, event)
- }
- return when (event.action) {
- MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
- v.parent.requestDisallowInterceptTouchEvent(player.isFullscreen)
- true
- }
- MotionEvent.ACTION_UP -> {
- v.parent.requestDisallowInterceptTouchEvent(false)
- false
- }
- else -> true
- }
- }
-
- private fun onTouchInPopup(v: View, event: MotionEvent): Boolean {
- player.gestureDetector.onTouchEvent(event)
- if (event.pointerCount == 2 && !isMovingInPopup && !isResizing) {
- if (DEBUG) {
- Log.d(TAG, "onTouch() 2 finger pointer detected, enabling resizing.")
- }
- onPopupResizingStart()
-
- // record coordinates of fingers
- initFirstPointerX = event.getX(0)
- initFirstPointerY = event.getY(0)
- initSecPointerX = event.getX(1)
- initSecPointerY = event.getY(1)
- // record distance between fingers
- initPointerDistance = hypot(
- initFirstPointerX - initSecPointerX.toDouble(),
- initFirstPointerY - initSecPointerY.toDouble()
- )
-
- isResizing = true
- }
- if (event.action == MotionEvent.ACTION_MOVE && !isMovingInPopup && isResizing) {
- if (DEBUG) {
- Log.d(
- TAG,
- "onTouch() ACTION_MOVE > v = [$v], e1.getRaw =" +
- "[${event.rawX}, ${event.rawY}]"
- )
- }
- return handleMultiDrag(event)
- }
- if (event.action == MotionEvent.ACTION_UP) {
- if (DEBUG) {
- Log.d(
- TAG,
- "onTouch() ACTION_UP > v = [$v], e1.getRaw =" +
- " [${event.rawX}, ${event.rawY}]"
- )
- }
- if (isMovingInPopup) {
- isMovingInPopup = false
- onScrollEnd(MainPlayer.PlayerType.POPUP, event)
- }
- if (isResizing) {
- isResizing = false
-
- initPointerDistance = (-1).toDouble()
- initFirstPointerX = (-1).toFloat()
- initFirstPointerY = (-1).toFloat()
- initSecPointerX = (-1).toFloat()
- initSecPointerY = (-1).toFloat()
-
- onPopupResizingEnd()
- player.changeState(player.currentState)
- }
- if (!player.isPopupClosing) {
- savePopupPositionAndSizeToPrefs(player)
- }
- }
-
- v.performClick()
- return true
- }
-
- private fun handleMultiDrag(event: MotionEvent): Boolean {
- if (initPointerDistance != -1.0 && event.pointerCount == 2) {
- // get the movements of the fingers
- val firstPointerMove = hypot(
- event.getX(0) - initFirstPointerX.toDouble(),
- event.getY(0) - initFirstPointerY.toDouble()
- )
- val secPointerMove = hypot(
- event.getX(1) - initSecPointerX.toDouble(),
- event.getY(1) - initSecPointerY.toDouble()
- )
-
- // minimum threshold beyond which pinch gesture will work
- val minimumMove = ViewConfiguration.get(service).scaledTouchSlop
-
- if (max(firstPointerMove, secPointerMove) > minimumMove) {
- // calculate current distance between the pointers
- val currentPointerDistance = hypot(
- event.getX(0) - event.getX(1).toDouble(),
- event.getY(0) - event.getY(1).toDouble()
- )
-
- val popupWidth = player.popupLayoutParams!!.width.toDouble()
- // change co-ordinates of popup so the center stays at the same position
- val newWidth = popupWidth * currentPointerDistance / initPointerDistance
- initPointerDistance = currentPointerDistance
- player.popupLayoutParams!!.x += ((popupWidth - newWidth) / 2.0).toInt()
-
- player.checkPopupPositionBounds()
- player.updateScreenSize()
- player.changePopupSize(min(player.screenWidth.toDouble(), newWidth).toInt())
- return true
- }
- }
- return false
- }
-
- // ///////////////////////////////////////////////////////////////////
- // Simple gestures
- // ///////////////////////////////////////////////////////////////////
-
- override fun onDown(e: MotionEvent): Boolean {
- if (DEBUG)
- Log.d(TAG, "onDown called with e = [$e]")
-
- if (isDoubleTapping && isDoubleTapEnabled) {
- doubleTapControls?.onDoubleTapProgressDown(getDisplayPortion(e))
- return true
- }
-
- return if (player.popupPlayerSelected())
- onDownInPopup(e)
- else
- true
- }
-
- private fun onDownInPopup(e: MotionEvent): Boolean {
- // Fix popup position when the user touch it, it may have the wrong one
- // because the soft input is visible (the draggable area is currently resized).
- player.updateScreenSize()
- player.checkPopupPositionBounds()
- player.popupLayoutParams?.let {
- initialPopupX = it.x
- initialPopupY = it.y
- }
- return super.onDown(e)
- }
-
- override fun onDoubleTap(e: MotionEvent): Boolean {
- if (DEBUG)
- Log.d(TAG, "onDoubleTap called with e = [$e]")
-
- onDoubleTap(e, getDisplayPortion(e))
- return true
- }
-
- override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
- if (DEBUG)
- Log.d(TAG, "onSingleTapConfirmed() called with: e = [$e]")
-
- if (isDoubleTapping)
- return true
-
- if (player.popupPlayerSelected()) {
- if (player.exoPlayerIsNull())
- return false
-
- onSingleTap(MainPlayer.PlayerType.POPUP)
- return true
- } else {
- super.onSingleTapConfirmed(e)
- if (player.currentState == Player.STATE_BLOCKED)
- return true
-
- onSingleTap(MainPlayer.PlayerType.VIDEO)
- }
- return true
- }
-
- override fun onLongPress(e: MotionEvent?) {
- if (player.popupPlayerSelected()) {
- player.updateScreenSize()
- player.checkPopupPositionBounds()
- player.changePopupSize(player.screenWidth.toInt())
- }
- }
-
- override fun onScroll(
- initialEvent: MotionEvent,
- movingEvent: MotionEvent,
- distanceX: Float,
- distanceY: Float
- ): Boolean {
- return if (player.popupPlayerSelected()) {
- onScrollInPopup(initialEvent, movingEvent, distanceX, distanceY)
- } else {
- onScrollInMain(initialEvent, movingEvent, distanceX, distanceY)
- }
- }
-
- override fun onFling(
- e1: MotionEvent?,
- e2: MotionEvent?,
- velocityX: Float,
- velocityY: Float
- ): Boolean {
- return if (player.popupPlayerSelected()) {
- val absVelocityX = abs(velocityX)
- val absVelocityY = abs(velocityY)
- if (absVelocityX.coerceAtLeast(absVelocityY) > tossFlingVelocity) {
- if (absVelocityX > tossFlingVelocity) {
- player.popupLayoutParams!!.x = velocityX.toInt()
- }
- if (absVelocityY > tossFlingVelocity) {
- player.popupLayoutParams!!.y = velocityY.toInt()
- }
- player.checkPopupPositionBounds()
- player.windowManager!!.updateViewLayout(player.rootView, player.popupLayoutParams)
- return true
- }
- return false
- } else {
- true
- }
- }
-
- private fun onScrollInMain(
- initialEvent: MotionEvent,
- movingEvent: MotionEvent,
- distanceX: Float,
- distanceY: Float
- ): Boolean {
-
- if (!player.isFullscreen) {
- return false
- }
-
- val isTouchingStatusBar: Boolean = initialEvent.y < getStatusBarHeight(service)
- val isTouchingNavigationBar: Boolean =
- initialEvent.y > (player.rootView.height - getNavigationBarHeight(service))
- if (isTouchingStatusBar || isTouchingNavigationBar) {
- return false
- }
-
- val insideThreshold = abs(movingEvent.y - initialEvent.y) <= MOVEMENT_THRESHOLD
- if (
- !isMovingInMain && (insideThreshold || abs(distanceX) > abs(distanceY)) ||
- player.currentState == Player.STATE_COMPLETED
- ) {
- return false
- }
-
- isMovingInMain = true
-
- onScroll(
- MainPlayer.PlayerType.VIDEO,
- getDisplayHalfPortion(initialEvent),
- initialEvent,
- movingEvent,
- distanceX,
- distanceY
- )
-
- return true
- }
-
- private fun onScrollInPopup(
- initialEvent: MotionEvent,
- movingEvent: MotionEvent,
- distanceX: Float,
- distanceY: Float
- ): Boolean {
-
- if (isResizing) {
- return super.onScroll(initialEvent, movingEvent, distanceX, distanceY)
- }
-
- if (!isMovingInPopup) {
- player.closeOverlayButton.animate(true, 200)
- }
-
- isMovingInPopup = true
-
- val diffX: Float = (movingEvent.rawX - initialEvent.rawX)
- var posX: Float = (initialPopupX + diffX)
- val diffY: Float = (movingEvent.rawY - initialEvent.rawY)
- var posY: Float = (initialPopupY + diffY)
-
- if (posX > player.screenWidth - player.popupLayoutParams!!.width) {
- posX = (player.screenWidth - player.popupLayoutParams!!.width)
- } else if (posX < 0) {
- posX = 0f
- }
-
- if (posY > player.screenHeight - player.popupLayoutParams!!.height) {
- posY = (player.screenHeight - player.popupLayoutParams!!.height)
- } else if (posY < 0) {
- posY = 0f
- }
-
- player.popupLayoutParams!!.x = posX.toInt()
- player.popupLayoutParams!!.y = posY.toInt()
-
- onScroll(
- MainPlayer.PlayerType.POPUP,
- getDisplayHalfPortion(initialEvent),
- initialEvent,
- movingEvent,
- distanceX,
- distanceY
- )
-
- player.windowManager!!.updateViewLayout(player.rootView, player.popupLayoutParams)
- return true
- }
-
- // ///////////////////////////////////////////////////////////////////
- // Multi double tapping
- // ///////////////////////////////////////////////////////////////////
-
- var doubleTapControls: DoubleTapListener? = null
- private set
-
- private val isDoubleTapEnabled: Boolean
- get() = doubleTapDelay > 0
-
- var isDoubleTapping = false
- private set
-
- fun doubleTapControls(listener: DoubleTapListener) = apply {
- doubleTapControls = listener
- }
-
- private var doubleTapDelay = DOUBLE_TAP_DELAY
- private val doubleTapHandler: Handler = Handler()
- private val doubleTapRunnable = Runnable {
- if (DEBUG)
- Log.d(TAG, "doubleTapRunnable called")
-
- isDoubleTapping = false
- doubleTapControls?.onDoubleTapFinished()
- }
-
- fun startMultiDoubleTap(e: MotionEvent) {
- if (!isDoubleTapping) {
- if (DEBUG)
- Log.d(TAG, "startMultiDoubleTap called with e = [$e]")
-
- keepInDoubleTapMode()
- doubleTapControls?.onDoubleTapStarted(getDisplayPortion(e))
- }
- }
-
- fun keepInDoubleTapMode() {
- if (DEBUG)
- Log.d(TAG, "keepInDoubleTapMode called")
-
- isDoubleTapping = true
- doubleTapHandler.removeCallbacks(doubleTapRunnable)
- doubleTapHandler.postDelayed(doubleTapRunnable, doubleTapDelay)
- }
-
- fun endMultiDoubleTap() {
- if (DEBUG)
- Log.d(TAG, "endMultiDoubleTap called")
-
- isDoubleTapping = false
- doubleTapHandler.removeCallbacks(doubleTapRunnable)
- doubleTapControls?.onDoubleTapFinished()
- }
-
- // ///////////////////////////////////////////////////////////////////
- // Utils
- // ///////////////////////////////////////////////////////////////////
-
- private fun getDisplayPortion(e: MotionEvent): DisplayPortion {
- return if (player.playerType == MainPlayer.PlayerType.POPUP && player.popupLayoutParams != null) {
- when {
- e.x < player.popupLayoutParams!!.width / 3.0 -> DisplayPortion.LEFT
- e.x > player.popupLayoutParams!!.width * 2.0 / 3.0 -> DisplayPortion.RIGHT
- else -> DisplayPortion.MIDDLE
- }
- } else /* MainPlayer.PlayerType.VIDEO */ {
- when {
- e.x < player.rootView.width / 3.0 -> DisplayPortion.LEFT
- e.x > player.rootView.width * 2.0 / 3.0 -> DisplayPortion.RIGHT
- else -> DisplayPortion.MIDDLE
- }
- }
- }
-
- // Currently needed for scrolling since there is no action more the middle portion
- private fun getDisplayHalfPortion(e: MotionEvent): DisplayPortion {
- return if (player.playerType == MainPlayer.PlayerType.POPUP) {
- when {
- e.x < player.popupLayoutParams!!.width / 2.0 -> DisplayPortion.LEFT_HALF
- else -> DisplayPortion.RIGHT_HALF
- }
- } else /* MainPlayer.PlayerType.VIDEO */ {
- when {
- e.x < player.rootView.width / 2.0 -> DisplayPortion.LEFT_HALF
- else -> DisplayPortion.RIGHT_HALF
- }
- }
- }
-
- private fun getNavigationBarHeight(context: Context): Int {
- val resId = context.resources
- .getIdentifier("navigation_bar_height", "dimen", "android")
- return if (resId > 0) {
- context.resources.getDimensionPixelSize(resId)
- } else 0
- }
-
- private fun getStatusBarHeight(context: Context): Int {
- val resId = context.resources
- .getIdentifier("status_bar_height", "dimen", "android")
- return if (resId > 0) {
- context.resources.getDimensionPixelSize(resId)
- } else 0
- }
-
- companion object {
- private const val TAG = "BasePlayerGestListener"
- private val DEBUG = Player.DEBUG
-
- private const val DOUBLE_TAP_DELAY = 550L
- private const val MOVEMENT_THRESHOLD = 40
- }
-}
diff --git a/app/src/main/java/org/schabi/newpipe/player/event/PlayerEventListener.java b/app/src/main/java/org/schabi/newpipe/player/event/PlayerEventListener.java
index b5520e8bee7..84bd9d277b3 100644
--- a/app/src/main/java/org/schabi/newpipe/player/event/PlayerEventListener.java
+++ b/app/src/main/java/org/schabi/newpipe/player/event/PlayerEventListener.java
@@ -1,6 +1,5 @@
package org.schabi.newpipe.player.event;
-
import com.google.android.exoplayer2.PlaybackParameters;
import org.schabi.newpipe.extractor.stream.StreamInfo;
diff --git a/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java b/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java
deleted file mode 100644
index a7fb40c47af..00000000000
--- a/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java
+++ /dev/null
@@ -1,256 +0,0 @@
-package org.schabi.newpipe.player.event;
-
-import static org.schabi.newpipe.ktx.AnimationType.ALPHA;
-import static org.schabi.newpipe.ktx.AnimationType.SCALE_AND_ALPHA;
-import static org.schabi.newpipe.ktx.ViewUtils.animate;
-import static org.schabi.newpipe.player.Player.DEFAULT_CONTROLS_DURATION;
-import static org.schabi.newpipe.player.Player.DEFAULT_CONTROLS_HIDE_TIME;
-import static org.schabi.newpipe.player.Player.STATE_PLAYING;
-
-import android.app.Activity;
-import android.util.Log;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.Window;
-import android.view.WindowManager;
-import android.widget.ProgressBar;
-
-import androidx.annotation.NonNull;
-import androidx.appcompat.content.res.AppCompatResources;
-
-import org.schabi.newpipe.MainActivity;
-import org.schabi.newpipe.R;
-import org.schabi.newpipe.player.MainPlayer;
-import org.schabi.newpipe.player.Player;
-import org.schabi.newpipe.player.helper.PlayerHelper;
-
-/**
- * GestureListener for the player
- *
- * While {@link BasePlayerGestureListener} contains the logic behind the single gestures
- * this class focuses on the visual aspect like hiding and showing the controls or changing
- * volume/brightness during scrolling for specific events.
- */
-public class PlayerGestureListener
- extends BasePlayerGestureListener
- implements View.OnTouchListener {
- private static final String TAG = PlayerGestureListener.class.getSimpleName();
- private static final boolean DEBUG = MainActivity.DEBUG;
-
- private final int maxVolume;
-
- public PlayerGestureListener(final Player player, final MainPlayer service) {
- super(player, service);
- maxVolume = player.getAudioReactor().getMaxVolume();
- }
-
- @Override
- public void onDoubleTap(@NonNull final MotionEvent event,
- @NonNull final DisplayPortion portion) {
- if (DEBUG) {
- Log.d(TAG, "onDoubleTap called with playerType = ["
- + player.getPlayerType() + "], portion = [" + portion + "]");
- }
- if (player.isSomePopupMenuVisible()) {
- player.hideControls(0, 0);
- }
-
- if (portion == DisplayPortion.LEFT || portion == DisplayPortion.RIGHT) {
- startMultiDoubleTap(event);
- } else if (portion == DisplayPortion.MIDDLE) {
- player.playPause();
- }
- }
-
- @Override
- public void onSingleTap(@NonNull final MainPlayer.PlayerType playerType) {
- if (DEBUG) {
- Log.d(TAG, "onSingleTap called with playerType = [" + player.getPlayerType() + "]");
- }
-
- if (player.isControlsVisible()) {
- player.hideControls(150, 0);
- return;
- }
- // -- Controls are not visible --
-
- // When player is completed show controls and don't hide them later
- if (player.getCurrentState() == Player.STATE_COMPLETED) {
- player.showControls(0);
- } else {
- player.showControlsThenHide();
- }
- }
-
- @Override
- public void onScroll(@NonNull final MainPlayer.PlayerType playerType,
- @NonNull final DisplayPortion portion,
- @NonNull final MotionEvent initialEvent,
- @NonNull final MotionEvent movingEvent,
- final float distanceX, final float distanceY) {
- if (DEBUG) {
- Log.d(TAG, "onScroll called with playerType = ["
- + player.getPlayerType() + "], portion = [" + portion + "]");
- }
- if (playerType == MainPlayer.PlayerType.VIDEO) {
-
- // -- Brightness and Volume control --
- final boolean isBrightnessGestureEnabled =
- PlayerHelper.isBrightnessGestureEnabled(service);
- final boolean isVolumeGestureEnabled = PlayerHelper.isVolumeGestureEnabled(service);
-
- if (isBrightnessGestureEnabled && isVolumeGestureEnabled) {
- if (portion == DisplayPortion.LEFT_HALF) {
- onScrollMainBrightness(distanceX, distanceY);
-
- } else /* DisplayPortion.RIGHT_HALF */ {
- onScrollMainVolume(distanceX, distanceY);
- }
- } else if (isBrightnessGestureEnabled) {
- onScrollMainBrightness(distanceX, distanceY);
- } else if (isVolumeGestureEnabled) {
- onScrollMainVolume(distanceX, distanceY);
- }
-
- } else /* MainPlayer.PlayerType.POPUP */ {
-
- // -- Determine if the ClosingOverlayView (red X) has to be shown or hidden --
- final View closingOverlayView = player.getClosingOverlayView();
- final boolean showClosingOverlayView = player.isInsideClosingRadius(movingEvent);
- // Check if an view is in expected state and if not animate it into the correct state
- final int expectedVisibility = showClosingOverlayView ? View.VISIBLE : View.GONE;
- if (closingOverlayView.getVisibility() != expectedVisibility) {
- animate(closingOverlayView, showClosingOverlayView, 200);
- }
- }
- }
-
- private void onScrollMainVolume(final float distanceX, final float distanceY) {
- // If we just started sliding, change the progress bar to match the system volume
- if (player.getVolumeRelativeLayout().getVisibility() != View.VISIBLE) {
- final float volumePercent = player
- .getAudioReactor().getVolume() / (float) maxVolume;
- player.getVolumeProgressBar().setProgress(
- (int) (volumePercent * player.getMaxGestureLength()));
- }
-
- player.getVolumeProgressBar().incrementProgressBy((int) distanceY);
- final float currentProgressPercent = (float) player
- .getVolumeProgressBar().getProgress() / player.getMaxGestureLength();
- final int currentVolume = (int) (maxVolume * currentProgressPercent);
- player.getAudioReactor().setVolume(currentVolume);
-
- if (DEBUG) {
- Log.d(TAG, "onScroll().volumeControl, currentVolume = " + currentVolume);
- }
-
- player.getVolumeImageView().setImageDrawable(
- AppCompatResources.getDrawable(service, currentProgressPercent <= 0
- ? R.drawable.ic_volume_off
- : currentProgressPercent < 0.25 ? R.drawable.ic_volume_mute
- : currentProgressPercent < 0.75 ? R.drawable.ic_volume_down
- : R.drawable.ic_volume_up)
- );
-
- if (player.getVolumeRelativeLayout().getVisibility() != View.VISIBLE) {
- animate(player.getVolumeRelativeLayout(), true, 200, SCALE_AND_ALPHA);
- }
- if (player.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) {
- player.getBrightnessRelativeLayout().setVisibility(View.GONE);
- }
- }
-
- private void onScrollMainBrightness(final float distanceX, final float distanceY) {
- final Activity parent = player.getParentActivity();
- if (parent == null) {
- return;
- }
-
- final Window window = parent.getWindow();
- final WindowManager.LayoutParams layoutParams = window.getAttributes();
- final ProgressBar bar = player.getBrightnessProgressBar();
- final float oldBrightness = layoutParams.screenBrightness;
- bar.setProgress((int) (bar.getMax() * Math.max(0, Math.min(1, oldBrightness))));
- bar.incrementProgressBy((int) distanceY);
-
- final float currentProgressPercent = (float) bar.getProgress() / bar.getMax();
- layoutParams.screenBrightness = currentProgressPercent;
- window.setAttributes(layoutParams);
-
- // Save current brightness level
- PlayerHelper.setScreenBrightness(parent, currentProgressPercent);
-
- if (DEBUG) {
- Log.d(TAG, "onScroll().brightnessControl, "
- + "currentBrightness = " + currentProgressPercent);
- }
-
- player.getBrightnessImageView().setImageDrawable(
- AppCompatResources.getDrawable(service,
- currentProgressPercent < 0.25
- ? R.drawable.ic_brightness_low
- : currentProgressPercent < 0.75
- ? R.drawable.ic_brightness_medium
- : R.drawable.ic_brightness_high)
- );
-
- if (player.getBrightnessRelativeLayout().getVisibility() != View.VISIBLE) {
- animate(player.getBrightnessRelativeLayout(), true, 200, SCALE_AND_ALPHA);
- }
- if (player.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) {
- player.getVolumeRelativeLayout().setVisibility(View.GONE);
- }
- }
-
- @Override
- public void onScrollEnd(@NonNull final MainPlayer.PlayerType playerType,
- @NonNull final MotionEvent event) {
- if (DEBUG) {
- Log.d(TAG, "onScrollEnd called with playerType = ["
- + player.getPlayerType() + "]");
- }
-
- if (player.isControlsVisible() && player.getCurrentState() == STATE_PLAYING) {
- player.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
- }
-
- if (playerType == MainPlayer.PlayerType.VIDEO) {
- if (player.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) {
- animate(player.getVolumeRelativeLayout(), false, 200, SCALE_AND_ALPHA,
- 200);
- }
- if (player.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) {
- animate(player.getBrightnessRelativeLayout(), false, 200, SCALE_AND_ALPHA,
- 200);
- }
- } else /* Popup-Player */ {
- if (player.isInsideClosingRadius(event)) {
- player.closePopup();
- } else if (!player.isPopupClosing()) {
- animate(player.getCloseOverlayButton(), false, 200);
- animate(player.getClosingOverlayView(), false, 200);
- }
- }
- }
-
- @Override
- public void onPopupResizingStart() {
- if (DEBUG) {
- Log.d(TAG, "onPopupResizingStart called");
- }
- player.getLoadingPanel().setVisibility(View.GONE);
-
- player.hideControls(0, 0);
- animate(player.getFastSeekOverlay(), false, 0);
- animate(player.getCurrentDisplaySeek(), false, 0, ALPHA, 0);
- }
-
- @Override
- public void onPopupResizingEnd() {
- if (DEBUG) {
- Log.d(TAG, "onPopupResizingEnd called");
- }
- }
-}
-
-
diff --git a/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceEventListener.java b/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceEventListener.java
index 359eab8b28e..8c18fd2ad1c 100644
--- a/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceEventListener.java
+++ b/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceEventListener.java
@@ -3,6 +3,8 @@
import com.google.android.exoplayer2.PlaybackException;
public interface PlayerServiceEventListener extends PlayerEventListener {
+ void onViewCreated();
+
void onFullscreenStateChanged(boolean fullscreen);
void onScreenRotationButtonClicked();
diff --git a/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceExtendedEventListener.java b/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceExtendedEventListener.java
index f774c90a0e7..8effe2f0e93 100644
--- a/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceExtendedEventListener.java
+++ b/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceExtendedEventListener.java
@@ -1,11 +1,11 @@
package org.schabi.newpipe.player.event;
-import org.schabi.newpipe.player.MainPlayer;
+import org.schabi.newpipe.player.PlayerService;
import org.schabi.newpipe.player.Player;
public interface PlayerServiceExtendedEventListener extends PlayerServiceEventListener {
void onServiceConnected(Player player,
- MainPlayer playerService,
+ PlayerService playerService,
boolean playAfterConnect);
void onServiceDisconnected();
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/gesture/BasePlayerGestureListener.kt b/app/src/main/java/org/schabi/newpipe/player/gesture/BasePlayerGestureListener.kt
new file mode 100644
index 00000000000..bd5d6f1c5f5
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/gesture/BasePlayerGestureListener.kt
@@ -0,0 +1,182 @@
+package org.schabi.newpipe.player.gesture
+
+import android.os.Handler
+import android.util.Log
+import android.view.GestureDetector
+import android.view.MotionEvent
+import android.view.View
+import org.schabi.newpipe.databinding.PlayerBinding
+import org.schabi.newpipe.player.Player
+import org.schabi.newpipe.player.ui.VideoPlayerUi
+
+/**
+ * Base gesture handling for [Player]
+ *
+ * This class contains the logic for the player gestures like View preparations
+ * and provides some abstract methods to make it easier separating the logic from the UI.
+ */
+abstract class BasePlayerGestureListener(
+ private val playerUi: VideoPlayerUi,
+) : GestureDetector.SimpleOnGestureListener(), View.OnTouchListener {
+
+ protected val player: Player = playerUi.player
+ protected val binding: PlayerBinding = playerUi.binding
+
+ override fun onTouch(v: View, event: MotionEvent): Boolean {
+ playerUi.gestureDetector.onTouchEvent(event)
+ return false
+ }
+
+ private fun onDoubleTap(
+ event: MotionEvent,
+ portion: DisplayPortion
+ ) {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "onDoubleTap called with playerType = [" +
+ player.playerType + "], portion = [" + portion + "]"
+ )
+ }
+ if (playerUi.isSomePopupMenuVisible) {
+ playerUi.hideControls(0, 0)
+ }
+ if (portion === DisplayPortion.LEFT || portion === DisplayPortion.RIGHT) {
+ startMultiDoubleTap(event)
+ } else if (portion === DisplayPortion.MIDDLE) {
+ player.playPause()
+ }
+ }
+
+ protected fun onSingleTap() {
+ if (playerUi.isControlsVisible) {
+ playerUi.hideControls(150, 0)
+ return
+ }
+ // -- Controls are not visible --
+
+ // When player is completed show controls and don't hide them later
+ if (player.currentState == Player.STATE_COMPLETED) {
+ playerUi.showControls(0)
+ } else {
+ playerUi.showControlsThenHide()
+ }
+ }
+
+ open fun onScrollEnd(event: MotionEvent) {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "onScrollEnd called with playerType = [" +
+ player.playerType + "]"
+ )
+ }
+ if (playerUi.isControlsVisible && player.currentState == Player.STATE_PLAYING) {
+ playerUi.hideControls(
+ VideoPlayerUi.DEFAULT_CONTROLS_DURATION,
+ VideoPlayerUi.DEFAULT_CONTROLS_HIDE_TIME
+ )
+ }
+ }
+
+ // ///////////////////////////////////////////////////////////////////
+ // Simple gestures
+ // ///////////////////////////////////////////////////////////////////
+
+ override fun onDown(e: MotionEvent): Boolean {
+ if (DEBUG)
+ Log.d(TAG, "onDown called with e = [$e]")
+
+ if (isDoubleTapping && isDoubleTapEnabled) {
+ doubleTapControls?.onDoubleTapProgressDown(getDisplayPortion(e))
+ return true
+ }
+
+ return if (onDownNotDoubleTapping(e)) super.onDown(e) else true
+ }
+
+ /**
+ * @return true if `super.onDown(e)` should be called, false otherwise
+ */
+ open fun onDownNotDoubleTapping(e: MotionEvent): Boolean {
+ return false // do not call super.onDown(e) by default, overridden for popup player
+ }
+
+ override fun onDoubleTap(e: MotionEvent): Boolean {
+ if (DEBUG)
+ Log.d(TAG, "onDoubleTap called with e = [$e]")
+
+ onDoubleTap(e, getDisplayPortion(e))
+ return true
+ }
+
+ // ///////////////////////////////////////////////////////////////////
+ // Multi double tapping
+ // ///////////////////////////////////////////////////////////////////
+
+ private var doubleTapControls: DoubleTapListener? = null
+
+ private val isDoubleTapEnabled: Boolean
+ get() = doubleTapDelay > 0
+
+ var isDoubleTapping = false
+ private set
+
+ fun doubleTapControls(listener: DoubleTapListener) = apply {
+ doubleTapControls = listener
+ }
+
+ private var doubleTapDelay = DOUBLE_TAP_DELAY
+ private val doubleTapHandler: Handler = Handler()
+ private val doubleTapRunnable = Runnable {
+ if (DEBUG)
+ Log.d(TAG, "doubleTapRunnable called")
+
+ isDoubleTapping = false
+ doubleTapControls?.onDoubleTapFinished()
+ }
+
+ private fun startMultiDoubleTap(e: MotionEvent) {
+ if (!isDoubleTapping) {
+ if (DEBUG)
+ Log.d(TAG, "startMultiDoubleTap called with e = [$e]")
+
+ keepInDoubleTapMode()
+ doubleTapControls?.onDoubleTapStarted(getDisplayPortion(e))
+ }
+ }
+
+ fun keepInDoubleTapMode() {
+ if (DEBUG)
+ Log.d(TAG, "keepInDoubleTapMode called")
+
+ isDoubleTapping = true
+ doubleTapHandler.removeCallbacks(doubleTapRunnable)
+ doubleTapHandler.postDelayed(doubleTapRunnable, doubleTapDelay)
+ }
+
+ fun endMultiDoubleTap() {
+ if (DEBUG)
+ Log.d(TAG, "endMultiDoubleTap called")
+
+ isDoubleTapping = false
+ doubleTapHandler.removeCallbacks(doubleTapRunnable)
+ doubleTapControls?.onDoubleTapFinished()
+ }
+
+ // ///////////////////////////////////////////////////////////////////
+ // Utils
+ // ///////////////////////////////////////////////////////////////////
+
+ abstract fun getDisplayPortion(e: MotionEvent): DisplayPortion
+
+ // Currently needed for scrolling since there is no action more the middle portion
+ abstract fun getDisplayHalfPortion(e: MotionEvent): DisplayPortion
+
+ companion object {
+ private const val TAG = "BasePlayerGestListener"
+ private val DEBUG = Player.DEBUG
+
+ private const val DOUBLE_TAP_DELAY = 550L
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/player/event/CustomBottomSheetBehavior.java b/app/src/main/java/org/schabi/newpipe/player/gesture/CustomBottomSheetBehavior.java
similarity index 98%
rename from app/src/main/java/org/schabi/newpipe/player/event/CustomBottomSheetBehavior.java
rename to app/src/main/java/org/schabi/newpipe/player/gesture/CustomBottomSheetBehavior.java
index a5de56e7569..2400091057a 100644
--- a/app/src/main/java/org/schabi/newpipe/player/event/CustomBottomSheetBehavior.java
+++ b/app/src/main/java/org/schabi/newpipe/player/gesture/CustomBottomSheetBehavior.java
@@ -1,4 +1,4 @@
-package org.schabi.newpipe.player.event;
+package org.schabi.newpipe.player.gesture;
import android.content.Context;
import android.graphics.Rect;
diff --git a/app/src/main/java/org/schabi/newpipe/player/event/DisplayPortion.kt b/app/src/main/java/org/schabi/newpipe/player/gesture/DisplayPortion.kt
similarity index 65%
rename from app/src/main/java/org/schabi/newpipe/player/event/DisplayPortion.kt
rename to app/src/main/java/org/schabi/newpipe/player/gesture/DisplayPortion.kt
index f15e42897ca..684f6d326f3 100644
--- a/app/src/main/java/org/schabi/newpipe/player/event/DisplayPortion.kt
+++ b/app/src/main/java/org/schabi/newpipe/player/gesture/DisplayPortion.kt
@@ -1,4 +1,4 @@
-package org.schabi.newpipe.player.event
+package org.schabi.newpipe.player.gesture
enum class DisplayPortion {
LEFT, MIDDLE, RIGHT, LEFT_HALF, RIGHT_HALF
diff --git a/app/src/main/java/org/schabi/newpipe/player/event/DoubleTapListener.kt b/app/src/main/java/org/schabi/newpipe/player/gesture/DoubleTapListener.kt
similarity index 81%
rename from app/src/main/java/org/schabi/newpipe/player/event/DoubleTapListener.kt
rename to app/src/main/java/org/schabi/newpipe/player/gesture/DoubleTapListener.kt
index 84cfb9b8d5a..1a0b141e648 100644
--- a/app/src/main/java/org/schabi/newpipe/player/event/DoubleTapListener.kt
+++ b/app/src/main/java/org/schabi/newpipe/player/gesture/DoubleTapListener.kt
@@ -1,4 +1,4 @@
-package org.schabi.newpipe.player.event
+package org.schabi.newpipe.player.gesture
interface DoubleTapListener {
fun onDoubleTapStarted(portion: DisplayPortion) {}
diff --git a/app/src/main/java/org/schabi/newpipe/player/gesture/MainPlayerGestureListener.kt b/app/src/main/java/org/schabi/newpipe/player/gesture/MainPlayerGestureListener.kt
new file mode 100644
index 00000000000..17205fb9ae5
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/gesture/MainPlayerGestureListener.kt
@@ -0,0 +1,232 @@
+package org.schabi.newpipe.player.gesture
+
+import android.app.Activity
+import android.content.Context
+import android.util.Log
+import android.view.MotionEvent
+import android.view.View
+import android.view.View.OnTouchListener
+import android.widget.ProgressBar
+import androidx.appcompat.content.res.AppCompatResources
+import org.schabi.newpipe.MainActivity
+import org.schabi.newpipe.R
+import org.schabi.newpipe.ktx.AnimationType
+import org.schabi.newpipe.ktx.animate
+import org.schabi.newpipe.player.Player
+import org.schabi.newpipe.player.helper.PlayerHelper
+import org.schabi.newpipe.player.ui.MainPlayerUi
+import kotlin.math.abs
+import kotlin.math.max
+import kotlin.math.min
+
+/**
+ * GestureListener for the player
+ *
+ * While [BasePlayerGestureListener] contains the logic behind the single gestures
+ * this class focuses on the visual aspect like hiding and showing the controls or changing
+ * volume/brightness during scrolling for specific events.
+ */
+class MainPlayerGestureListener(
+ private val playerUi: MainPlayerUi
+) : BasePlayerGestureListener(playerUi), OnTouchListener {
+ private val maxVolume: Int = player.audioReactor.maxVolume
+
+ private var isMoving = false
+
+ override fun onTouch(v: View, event: MotionEvent): Boolean {
+ super.onTouch(v, event)
+ if (event.action == MotionEvent.ACTION_UP && isMoving) {
+ isMoving = false
+ onScrollEnd(event)
+ }
+ return when (event.action) {
+ MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
+ v.parent.requestDisallowInterceptTouchEvent(playerUi.isFullscreen)
+ true
+ }
+ MotionEvent.ACTION_UP -> {
+ v.parent.requestDisallowInterceptTouchEvent(false)
+ false
+ }
+ else -> true
+ }
+ }
+
+ override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
+ if (DEBUG)
+ Log.d(TAG, "onSingleTapConfirmed() called with: e = [$e]")
+
+ if (isDoubleTapping)
+ return true
+ super.onSingleTapConfirmed(e)
+
+ if (player.currentState != Player.STATE_BLOCKED)
+ onSingleTap()
+ return true
+ }
+
+ private fun onScrollVolume(distanceY: Float) {
+ // If we just started sliding, change the progress bar to match the system volume
+ if (binding.volumeRelativeLayout.visibility != View.VISIBLE) {
+ val volumePercent: Float = player.audioReactor.volume / maxVolume.toFloat()
+ binding.volumeProgressBar.progress = (volumePercent * MAX_GESTURE_LENGTH).toInt()
+ }
+
+ binding.volumeProgressBar.incrementProgressBy(distanceY.toInt())
+ val currentProgressPercent: Float =
+ binding.volumeProgressBar.progress.toFloat() / MAX_GESTURE_LENGTH
+ val currentVolume = (maxVolume * currentProgressPercent).toInt()
+ player.audioReactor.volume = currentVolume
+ if (DEBUG) {
+ Log.d(TAG, "onScroll().volumeControl, currentVolume = $currentVolume")
+ }
+
+ binding.volumeImageView.setImageDrawable(
+ AppCompatResources.getDrawable(
+ player.context,
+ when {
+ currentProgressPercent <= 0 -> R.drawable.ic_volume_off
+ currentProgressPercent < 0.25 -> R.drawable.ic_volume_mute
+ currentProgressPercent < 0.75 -> R.drawable.ic_volume_down
+ else -> R.drawable.ic_volume_up
+ }
+ )
+ )
+
+ if (binding.volumeRelativeLayout.visibility != View.VISIBLE) {
+ binding.volumeRelativeLayout.animate(true, 200, AnimationType.SCALE_AND_ALPHA)
+ }
+ if (binding.brightnessRelativeLayout.visibility == View.VISIBLE) {
+ binding.volumeRelativeLayout.visibility = View.GONE
+ }
+ }
+
+ private fun onScrollBrightness(distanceY: Float) {
+ val parent: Activity = playerUi.parentActivity
+ val window = parent.window
+ val layoutParams = window.attributes
+ val bar: ProgressBar = binding.brightnessProgressBar
+ val oldBrightness = layoutParams.screenBrightness
+ bar.progress = (bar.max * max(0f, min(1f, oldBrightness))).toInt()
+ bar.incrementProgressBy(distanceY.toInt())
+ val currentProgressPercent = bar.progress.toFloat() / bar.max
+ layoutParams.screenBrightness = currentProgressPercent
+ window.attributes = layoutParams
+
+ // Save current brightness level
+ PlayerHelper.setScreenBrightness(parent, currentProgressPercent)
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "onScroll().brightnessControl, " +
+ "currentBrightness = " + currentProgressPercent
+ )
+ }
+ binding.brightnessImageView.setImageDrawable(
+ AppCompatResources.getDrawable(
+ player.context,
+ if (currentProgressPercent < 0.25) R.drawable.ic_brightness_low else if (currentProgressPercent < 0.75) R.drawable.ic_brightness_medium else R.drawable.ic_brightness_high
+ )
+ )
+ if (binding.brightnessRelativeLayout.visibility != View.VISIBLE) {
+ binding.brightnessRelativeLayout.animate(true, 200, AnimationType.SCALE_AND_ALPHA)
+ }
+ if (binding.volumeRelativeLayout.visibility == View.VISIBLE) {
+ binding.volumeRelativeLayout.visibility = View.GONE
+ }
+ }
+
+ override fun onScrollEnd(event: MotionEvent) {
+ super.onScrollEnd(event)
+ if (binding.volumeRelativeLayout.visibility == View.VISIBLE) {
+ binding.volumeRelativeLayout.animate(false, 200, AnimationType.SCALE_AND_ALPHA, 200)
+ }
+ if (binding.brightnessRelativeLayout.visibility == View.VISIBLE) {
+ binding.brightnessRelativeLayout.animate(false, 200, AnimationType.SCALE_AND_ALPHA, 200)
+ }
+ }
+
+ override fun onScroll(
+ initialEvent: MotionEvent,
+ movingEvent: MotionEvent,
+ distanceX: Float,
+ distanceY: Float
+ ): Boolean {
+
+ if (!playerUi.isFullscreen) {
+ return false
+ }
+
+ val isTouchingStatusBar: Boolean = initialEvent.y < getStatusBarHeight(player.context)
+ val isTouchingNavigationBar: Boolean =
+ initialEvent.y > (binding.root.height - getNavigationBarHeight(player.context))
+ if (isTouchingStatusBar || isTouchingNavigationBar) {
+ return false
+ }
+
+ val insideThreshold = abs(movingEvent.y - initialEvent.y) <= MOVEMENT_THRESHOLD
+ if (
+ !isMoving && (insideThreshold || abs(distanceX) > abs(distanceY)) ||
+ player.currentState == Player.STATE_COMPLETED
+ ) {
+ return false
+ }
+
+ isMoving = true
+
+ // -- Brightness and Volume control --
+ val isBrightnessGestureEnabled = PlayerHelper.isBrightnessGestureEnabled(player.context)
+ val isVolumeGestureEnabled = PlayerHelper.isVolumeGestureEnabled(player.context)
+ if (isBrightnessGestureEnabled && isVolumeGestureEnabled) {
+ if (getDisplayHalfPortion(initialEvent) === DisplayPortion.LEFT_HALF) {
+ onScrollBrightness(distanceY)
+ } else /* DisplayPortion.RIGHT_HALF */ {
+ onScrollVolume(distanceY)
+ }
+ } else if (isBrightnessGestureEnabled) {
+ onScrollBrightness(distanceY)
+ } else if (isVolumeGestureEnabled) {
+ onScrollVolume(distanceY)
+ }
+
+ return true
+ }
+
+ override fun getDisplayPortion(e: MotionEvent): DisplayPortion {
+ return when {
+ e.x < binding.root.width / 3.0 -> DisplayPortion.LEFT
+ e.x > binding.root.width * 2.0 / 3.0 -> DisplayPortion.RIGHT
+ else -> DisplayPortion.MIDDLE
+ }
+ }
+
+ override fun getDisplayHalfPortion(e: MotionEvent): DisplayPortion {
+ return when {
+ e.x < binding.root.width / 2.0 -> DisplayPortion.LEFT_HALF
+ else -> DisplayPortion.RIGHT_HALF
+ }
+ }
+
+ companion object {
+ private val TAG = MainPlayerGestureListener::class.java.simpleName
+ private val DEBUG = MainActivity.DEBUG
+ private const val MOVEMENT_THRESHOLD = 40
+ const val MAX_GESTURE_LENGTH = 0.75f
+
+ private fun getNavigationBarHeight(context: Context): Int {
+ val resId = context.resources
+ .getIdentifier("navigation_bar_height", "dimen", "android")
+ return if (resId > 0) {
+ context.resources.getDimensionPixelSize(resId)
+ } else 0
+ }
+
+ private fun getStatusBarHeight(context: Context): Int {
+ val resId = context.resources
+ .getIdentifier("status_bar_height", "dimen", "android")
+ return if (resId > 0) {
+ context.resources.getDimensionPixelSize(resId)
+ } else 0
+ }
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/player/gesture/PopupPlayerGestureListener.kt b/app/src/main/java/org/schabi/newpipe/player/gesture/PopupPlayerGestureListener.kt
new file mode 100644
index 00000000000..b8c1bc54c5f
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/gesture/PopupPlayerGestureListener.kt
@@ -0,0 +1,287 @@
+package org.schabi.newpipe.player.gesture
+
+import android.util.Log
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewConfiguration
+import org.schabi.newpipe.MainActivity
+import org.schabi.newpipe.ktx.AnimationType
+import org.schabi.newpipe.ktx.animate
+import org.schabi.newpipe.player.helper.PlayerHelper
+import org.schabi.newpipe.player.ui.PopupPlayerUi
+import kotlin.math.abs
+import kotlin.math.hypot
+import kotlin.math.max
+import kotlin.math.min
+
+class PopupPlayerGestureListener(
+ private val playerUi: PopupPlayerUi,
+) : BasePlayerGestureListener(playerUi) {
+
+ private var isMoving = false
+
+ private var initialPopupX: Int = -1
+ private var initialPopupY: Int = -1
+ private var isResizing = false
+
+ // initial coordinates and distance between fingers
+ private var initPointerDistance = -1.0
+ private var initFirstPointerX = -1f
+ private var initFirstPointerY = -1f
+ private var initSecPointerX = -1f
+ private var initSecPointerY = -1f
+
+ override fun onTouch(v: View, event: MotionEvent): Boolean {
+ super.onTouch(v, event)
+ if (event.pointerCount == 2 && !isMoving && !isResizing) {
+ if (DEBUG) {
+ Log.d(TAG, "onTouch() 2 finger pointer detected, enabling resizing.")
+ }
+ onPopupResizingStart()
+
+ // record coordinates of fingers
+ initFirstPointerX = event.getX(0)
+ initFirstPointerY = event.getY(0)
+ initSecPointerX = event.getX(1)
+ initSecPointerY = event.getY(1)
+ // record distance between fingers
+ initPointerDistance = hypot(
+ initFirstPointerX - initSecPointerX.toDouble(),
+ initFirstPointerY - initSecPointerY.toDouble()
+ )
+
+ isResizing = true
+ }
+ if (event.action == MotionEvent.ACTION_MOVE && !isMoving && isResizing) {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "onTouch() ACTION_MOVE > v = [$v], e1.getRaw =" +
+ "[${event.rawX}, ${event.rawY}]"
+ )
+ }
+ return handleMultiDrag(event)
+ }
+ if (event.action == MotionEvent.ACTION_UP) {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "onTouch() ACTION_UP > v = [$v], e1.getRaw =" +
+ " [${event.rawX}, ${event.rawY}]"
+ )
+ }
+ if (isMoving) {
+ isMoving = false
+ onScrollEnd(event)
+ }
+ if (isResizing) {
+ isResizing = false
+
+ initPointerDistance = (-1).toDouble()
+ initFirstPointerX = (-1).toFloat()
+ initFirstPointerY = (-1).toFloat()
+ initSecPointerX = (-1).toFloat()
+ initSecPointerY = (-1).toFloat()
+
+ onPopupResizingEnd()
+ player.changeState(player.currentState)
+ }
+ if (!playerUi.isPopupClosing) {
+ PlayerHelper.savePopupPositionAndSizeToPrefs(playerUi)
+ }
+ }
+
+ v.performClick()
+ return true
+ }
+
+ override fun onScrollEnd(event: MotionEvent) {
+ super.onScrollEnd(event)
+ if (playerUi.isInsideClosingRadius(event)) {
+ playerUi.closePopup()
+ } else if (!playerUi.isPopupClosing) {
+ playerUi.closeOverlayBinding.closeButton.animate(false, 200)
+ binding.closingOverlay.animate(false, 200)
+ }
+ }
+
+ private fun handleMultiDrag(event: MotionEvent): Boolean {
+ if (initPointerDistance != -1.0 && event.pointerCount == 2) {
+ // get the movements of the fingers
+ val firstPointerMove = hypot(
+ event.getX(0) - initFirstPointerX.toDouble(),
+ event.getY(0) - initFirstPointerY.toDouble()
+ )
+ val secPointerMove = hypot(
+ event.getX(1) - initSecPointerX.toDouble(),
+ event.getY(1) - initSecPointerY.toDouble()
+ )
+
+ // minimum threshold beyond which pinch gesture will work
+ val minimumMove = ViewConfiguration.get(player.context).scaledTouchSlop
+
+ if (max(firstPointerMove, secPointerMove) > minimumMove) {
+ // calculate current distance between the pointers
+ val currentPointerDistance = hypot(
+ event.getX(0) - event.getX(1).toDouble(),
+ event.getY(0) - event.getY(1).toDouble()
+ )
+
+ val popupWidth = playerUi.popupLayoutParams.width.toDouble()
+ // change co-ordinates of popup so the center stays at the same position
+ val newWidth = popupWidth * currentPointerDistance / initPointerDistance
+ initPointerDistance = currentPointerDistance
+ playerUi.popupLayoutParams.x += ((popupWidth - newWidth) / 2.0).toInt()
+
+ playerUi.checkPopupPositionBounds()
+ playerUi.updateScreenSize()
+ playerUi.changePopupSize(min(playerUi.screenWidth.toDouble(), newWidth).toInt())
+ return true
+ }
+ }
+ return false
+ }
+
+ private fun onPopupResizingStart() {
+ if (DEBUG) {
+ Log.d(TAG, "onPopupResizingStart called")
+ }
+ binding.loadingPanel.visibility = View.GONE
+ playerUi.hideControls(0, 0)
+ binding.fastSeekOverlay.animate(false, 0)
+ binding.currentDisplaySeek.animate(false, 0, AnimationType.ALPHA, 0)
+ }
+
+ private fun onPopupResizingEnd() {
+ if (DEBUG) {
+ Log.d(TAG, "onPopupResizingEnd called")
+ }
+ }
+
+ override fun onLongPress(e: MotionEvent?) {
+ playerUi.updateScreenSize()
+ playerUi.checkPopupPositionBounds()
+ playerUi.changePopupSize(playerUi.screenWidth)
+ }
+
+ override fun onFling(
+ e1: MotionEvent?,
+ e2: MotionEvent?,
+ velocityX: Float,
+ velocityY: Float
+ ): Boolean {
+ return if (player.popupPlayerSelected()) {
+ val absVelocityX = abs(velocityX)
+ val absVelocityY = abs(velocityY)
+ if (absVelocityX.coerceAtLeast(absVelocityY) > TOSS_FLING_VELOCITY) {
+ if (absVelocityX > TOSS_FLING_VELOCITY) {
+ playerUi.popupLayoutParams.x = velocityX.toInt()
+ }
+ if (absVelocityY > TOSS_FLING_VELOCITY) {
+ playerUi.popupLayoutParams.y = velocityY.toInt()
+ }
+ playerUi.checkPopupPositionBounds()
+ playerUi.windowManager.updateViewLayout(binding.root, playerUi.popupLayoutParams)
+ return true
+ }
+ return false
+ } else {
+ true
+ }
+ }
+
+ override fun onDownNotDoubleTapping(e: MotionEvent): Boolean {
+ // Fix popup position when the user touch it, it may have the wrong one
+ // because the soft input is visible (the draggable area is currently resized).
+ playerUi.updateScreenSize()
+ playerUi.checkPopupPositionBounds()
+ playerUi.popupLayoutParams.let {
+ initialPopupX = it.x
+ initialPopupY = it.y
+ }
+ return true // we want `super.onDown(e)` to be called
+ }
+
+ override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
+ if (DEBUG)
+ Log.d(TAG, "onSingleTapConfirmed() called with: e = [$e]")
+
+ if (isDoubleTapping)
+ return true
+ if (player.exoPlayerIsNull())
+ return false
+
+ onSingleTap()
+ return true
+ }
+
+ override fun onScroll(
+ initialEvent: MotionEvent,
+ movingEvent: MotionEvent,
+ distanceX: Float,
+ distanceY: Float
+ ): Boolean {
+
+ if (isResizing) {
+ return super.onScroll(initialEvent, movingEvent, distanceX, distanceY)
+ }
+
+ if (!isMoving) {
+ playerUi.closeOverlayBinding.closeButton.animate(true, 200)
+ }
+
+ isMoving = true
+
+ val diffX: Float = (movingEvent.rawX - initialEvent.rawX)
+ var posX: Float = (initialPopupX + diffX)
+ val diffY: Float = (movingEvent.rawY - initialEvent.rawY)
+ var posY: Float = (initialPopupY + diffY)
+
+ if (posX > playerUi.screenWidth - playerUi.popupLayoutParams.width) {
+ posX = (playerUi.screenWidth - playerUi.popupLayoutParams.width).toFloat()
+ } else if (posX < 0) {
+ posX = 0f
+ }
+
+ if (posY > playerUi.screenHeight - playerUi.popupLayoutParams.height) {
+ posY = (playerUi.screenHeight - playerUi.popupLayoutParams.height).toFloat()
+ } else if (posY < 0) {
+ posY = 0f
+ }
+
+ playerUi.popupLayoutParams.x = posX.toInt()
+ playerUi.popupLayoutParams.y = posY.toInt()
+
+ // -- Determine if the ClosingOverlayView (red X) has to be shown or hidden --
+ val showClosingOverlayView: Boolean = playerUi.isInsideClosingRadius(movingEvent)
+ // Check if an view is in expected state and if not animate it into the correct state
+ val expectedVisibility = if (showClosingOverlayView) View.VISIBLE else View.GONE
+ if (binding.closingOverlay.visibility != expectedVisibility) {
+ binding.closingOverlay.animate(showClosingOverlayView, 200)
+ }
+
+ playerUi.windowManager.updateViewLayout(binding.root, playerUi.popupLayoutParams)
+ return true
+ }
+
+ override fun getDisplayPortion(e: MotionEvent): DisplayPortion {
+ return when {
+ e.x < playerUi.popupLayoutParams.width / 3.0 -> DisplayPortion.LEFT
+ e.x > playerUi.popupLayoutParams.width * 2.0 / 3.0 -> DisplayPortion.RIGHT
+ else -> DisplayPortion.MIDDLE
+ }
+ }
+
+ override fun getDisplayHalfPortion(e: MotionEvent): DisplayPortion {
+ return when {
+ e.x < playerUi.popupLayoutParams.width / 2.0 -> DisplayPortion.LEFT_HALF
+ else -> DisplayPortion.RIGHT_HALF
+ }
+ }
+
+ companion object {
+ private val TAG = PopupPlayerGestureListener::class.java.simpleName
+ private val DEBUG = MainActivity.DEBUG
+ private const val TOSS_FLING_VELOCITY = 2500
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java
index 19a5a645bbe..8a5a4f8d204 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java
@@ -26,7 +26,7 @@
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.DialogPlaybackParameterBinding;
-import org.schabi.newpipe.player.Player;
+import org.schabi.newpipe.player.ui.VideoPlayerUi;
import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener;
import org.schabi.newpipe.util.SliderStrategy;
@@ -207,7 +207,7 @@ private void initUI() {
? View.VISIBLE
: View.GONE);
animateRotation(binding.pitchToogleControlModes,
- Player.DEFAULT_CONTROLS_DURATION,
+ VideoPlayerUi.DEFAULT_CONTROLS_DURATION,
isCurrentlyVisible ? 180 : 0);
});
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java
index 2131861bff6..ec4cf8602a5 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java
@@ -3,7 +3,6 @@
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF;
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
-import static org.schabi.newpipe.player.Player.IDLE_WINDOW_FLAGS;
import static org.schabi.newpipe.player.Player.PLAYER_TYPE;
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_ALWAYS;
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_NEVER;
@@ -11,6 +10,7 @@
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND;
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE;
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_POPUP;
+import static org.schabi.newpipe.player.ui.PopupPlayerUi.IDLE_WINDOW_FLAGS;
import static java.lang.annotation.RetentionPolicy.SOURCE;
import android.annotation.SuppressLint;
@@ -49,11 +49,12 @@
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
import org.schabi.newpipe.extractor.utils.Utils;
-import org.schabi.newpipe.player.MainPlayer;
+import org.schabi.newpipe.player.PlayerService;
import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
+import org.schabi.newpipe.player.ui.PopupPlayerUi;
import org.schabi.newpipe.util.ListHelper;
import java.lang.annotation.Retention;
@@ -339,10 +340,6 @@ public static boolean isUsingDSP() {
return true;
}
- public static int getTossFlingVelocity() {
- return 2500;
- }
-
@NonNull
public static CaptionStyleCompat getCaptionStyle(@NonNull final Context context) {
final CaptioningManager captioningManager = ContextCompat.getSystemService(context,
@@ -452,10 +449,10 @@ private static SinglePlayQueue getAutoQueuedSinglePlayQueue(
// Utils used by player
////////////////////////////////////////////////////////////////////////////
- public static MainPlayer.PlayerType retrievePlayerTypeFromIntent(final Intent intent) {
+ public static PlayerService.PlayerType retrievePlayerTypeFromIntent(final Intent intent) {
// If you want to open popup from the app just include Constants.POPUP_ONLY into an extra
- return MainPlayer.PlayerType.values()[
- intent.getIntExtra(PLAYER_TYPE, MainPlayer.PlayerType.VIDEO.ordinal())];
+ return PlayerService.PlayerType.values()[
+ intent.getIntExtra(PLAYER_TYPE, PlayerService.PlayerType.MAIN.ordinal())];
}
public static boolean isPlaybackResumeEnabled(final Player player) {
@@ -529,19 +526,20 @@ public static void savePlaybackParametersToPrefs(final Player player,
}
/**
- * @param player {@code screenWidth} and {@code screenHeight} must have been initialized
+ * @param playerUi {@code screenWidth} and {@code screenHeight} must have been initialized
* @return the popup starting layout params
*/
@SuppressLint("RtlHardcoded")
public static WindowManager.LayoutParams retrievePopupLayoutParamsFromPrefs(
- final Player player) {
- final boolean popupRememberSizeAndPos = player.getPrefs().getBoolean(
- player.getContext().getString(R.string.popup_remember_size_pos_key), true);
- final float defaultSize =
- player.getContext().getResources().getDimension(R.dimen.popup_default_width);
+ final PopupPlayerUi playerUi) {
+ final SharedPreferences prefs = playerUi.getPlayer().getPrefs();
+ final Context context = playerUi.getPlayer().getContext();
+
+ final boolean popupRememberSizeAndPos = prefs.getBoolean(
+ context.getString(R.string.popup_remember_size_pos_key), true);
+ final float defaultSize = context.getResources().getDimension(R.dimen.popup_default_width);
final float popupWidth = popupRememberSizeAndPos
- ? player.getPrefs().getFloat(player.getContext().getString(
- R.string.popup_saved_width_key), defaultSize)
+ ? prefs.getFloat(context.getString(R.string.popup_saved_width_key), defaultSize)
: defaultSize;
final float popupHeight = getMinimumVideoHeight(popupWidth);
@@ -553,27 +551,26 @@ public static WindowManager.LayoutParams retrievePopupLayoutParamsFromPrefs(
popupLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
popupLayoutParams.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
- final int centerX = (int) (player.getScreenWidth() / 2f - popupWidth / 2f);
- final int centerY = (int) (player.getScreenHeight() / 2f - popupHeight / 2f);
+ final int centerX = (int) (playerUi.getScreenWidth() / 2f - popupWidth / 2f);
+ final int centerY = (int) (playerUi.getScreenHeight() / 2f - popupHeight / 2f);
popupLayoutParams.x = popupRememberSizeAndPos
- ? player.getPrefs().getInt(player.getContext().getString(
- R.string.popup_saved_x_key), centerX) : centerX;
+ ? prefs.getInt(context.getString(R.string.popup_saved_x_key), centerX) : centerX;
popupLayoutParams.y = popupRememberSizeAndPos
- ? player.getPrefs().getInt(player.getContext().getString(
- R.string.popup_saved_y_key), centerY) : centerY;
+ ? prefs.getInt(context.getString(R.string.popup_saved_y_key), centerY) : centerY;
return popupLayoutParams;
}
- public static void savePopupPositionAndSizeToPrefs(final Player player) {
- if (player.getPopupLayoutParams() != null) {
- player.getPrefs().edit()
- .putFloat(player.getContext().getString(R.string.popup_saved_width_key),
- player.getPopupLayoutParams().width)
- .putInt(player.getContext().getString(R.string.popup_saved_x_key),
- player.getPopupLayoutParams().x)
- .putInt(player.getContext().getString(R.string.popup_saved_y_key),
- player.getPopupLayoutParams().y)
+ public static void savePopupPositionAndSizeToPrefs(final PopupPlayerUi playerUi) {
+ if (playerUi.getPopupLayoutParams() != null) {
+ final Context context = playerUi.getPlayer().getContext();
+ playerUi.getPlayer().getPrefs().edit()
+ .putFloat(context.getString(R.string.popup_saved_width_key),
+ playerUi.getPopupLayoutParams().width)
+ .putInt(context.getString(R.string.popup_saved_x_key),
+ playerUi.getPopupLayoutParams().x)
+ .putInt(context.getString(R.string.popup_saved_y_key),
+ playerUi.getPopupLayoutParams().y)
.apply();
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java
index 4c09ed3c19a..cb613f8541e 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java
@@ -16,7 +16,7 @@
import org.schabi.newpipe.App;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.extractor.stream.StreamInfo;
-import org.schabi.newpipe.player.MainPlayer;
+import org.schabi.newpipe.player.PlayerService;
import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.event.PlayerServiceEventListener;
import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener;
@@ -42,17 +42,17 @@ public static synchronized PlayerHolder getInstance() {
private final PlayerServiceConnection serviceConnection = new PlayerServiceConnection();
private boolean bound;
- @Nullable private MainPlayer playerService;
+ @Nullable private PlayerService playerService;
@Nullable private Player player;
/**
- * Returns the current {@link MainPlayer.PlayerType} of the {@link MainPlayer} service,
+ * Returns the current {@link PlayerService.PlayerType} of the {@link PlayerService} service,
* otherwise `null` if no service running.
*
* @return Current PlayerType
*/
@Nullable
- public MainPlayer.PlayerType getType() {
+ public PlayerService.PlayerType getType() {
if (player == null) {
return null;
}
@@ -122,7 +122,7 @@ public void startService(final boolean playAfterConnect,
// and NullPointerExceptions inside the service because the service will be
// bound twice. Prevent it with unbinding first
unbind(context);
- ContextCompat.startForegroundService(context, new Intent(context, MainPlayer.class));
+ ContextCompat.startForegroundService(context, new Intent(context, PlayerService.class));
serviceConnection.doPlayAfterConnect(playAfterConnect);
bind(context);
}
@@ -130,7 +130,7 @@ public void startService(final boolean playAfterConnect,
public void stopService() {
final Context context = getCommonContext();
unbind(context);
- context.stopService(new Intent(context, MainPlayer.class));
+ context.stopService(new Intent(context, PlayerService.class));
}
class PlayerServiceConnection implements ServiceConnection {
@@ -156,7 +156,7 @@ public void onServiceConnected(final ComponentName compName, final IBinder servi
if (DEBUG) {
Log.d(TAG, "Player service is connected");
}
- final MainPlayer.LocalBinder localBinder = (MainPlayer.LocalBinder) service;
+ final PlayerService.LocalBinder localBinder = (PlayerService.LocalBinder) service;
playerService = localBinder.getService();
player = localBinder.getPlayer();
@@ -172,7 +172,7 @@ private void bind(final Context context) {
Log.d(TAG, "bind() called");
}
- final Intent serviceIntent = new Intent(context, MainPlayer.class);
+ final Intent serviceIntent = new Intent(context, PlayerService.class);
bound = context.bindService(serviceIntent, serviceConnection,
Context.BIND_AUTO_CREATE);
if (!bound) {
@@ -211,6 +211,13 @@ private void stopPlayerListener() {
private final PlayerServiceEventListener internalListener =
new PlayerServiceEventListener() {
+ @Override
+ public void onViewCreated() {
+ if (listener != null) {
+ listener.onViewCreated();
+ }
+ }
+
@Override
public void onFullscreenStateChanged(final boolean fullscreen) {
if (listener != null) {
diff --git a/app/src/main/java/org/schabi/newpipe/player/listeners/view/PlaybackSpeedClickListener.kt b/app/src/main/java/org/schabi/newpipe/player/listeners/view/PlaybackSpeedClickListener.kt
deleted file mode 100644
index 52eff5a1cce..00000000000
--- a/app/src/main/java/org/schabi/newpipe/player/listeners/view/PlaybackSpeedClickListener.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-package org.schabi.newpipe.player.listeners.view
-
-import android.util.Log
-import android.view.View
-import androidx.appcompat.widget.PopupMenu
-import org.schabi.newpipe.MainActivity
-import org.schabi.newpipe.player.Player
-import org.schabi.newpipe.player.helper.PlaybackParameterDialog
-
-/**
- * Click listener for the playbackSpeed textview of the player
- */
-class PlaybackSpeedClickListener(
- private val player: Player,
- private val playbackSpeedPopupMenu: PopupMenu
-) : View.OnClickListener {
-
- companion object {
- private const val TAG: String = "PlaybSpeedClickListener"
- }
-
- override fun onClick(v: View) {
- if (MainActivity.DEBUG) {
- Log.d(TAG, "onPlaybackSpeedClicked() called")
- }
-
- if (player.videoPlayerSelected()) {
- PlaybackParameterDialog.newInstance(
- player.playbackSpeed.toDouble(),
- player.playbackPitch.toDouble(),
- player.playbackSkipSilence
- ) { speed: Float, pitch: Float, skipSilence: Boolean ->
- player.setPlaybackParameters(
- speed,
- pitch,
- skipSilence
- )
- }
- .show(player.parentActivity!!.supportFragmentManager, null)
- } else {
- playbackSpeedPopupMenu.show()
- player.isSomePopupMenuVisible = true
- }
-
- player.manageControlsAfterOnClick(v)
- }
-}
diff --git a/app/src/main/java/org/schabi/newpipe/player/listeners/view/QualityClickListener.kt b/app/src/main/java/org/schabi/newpipe/player/listeners/view/QualityClickListener.kt
deleted file mode 100644
index 43e8288e605..00000000000
--- a/app/src/main/java/org/schabi/newpipe/player/listeners/view/QualityClickListener.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-package org.schabi.newpipe.player.listeners.view
-
-import android.annotation.SuppressLint
-import android.util.Log
-import android.view.View
-import androidx.appcompat.widget.PopupMenu
-import org.schabi.newpipe.MainActivity
-import org.schabi.newpipe.extractor.MediaFormat
-import org.schabi.newpipe.player.Player
-
-/**
- * Click listener for the qualityTextView of the player
- */
-class QualityClickListener(
- private val player: Player,
- private val qualityPopupMenu: PopupMenu
-) : View.OnClickListener {
-
- companion object {
- private const val TAG: String = "QualityClickListener"
- }
-
- @SuppressLint("SetTextI18n") // we don't need I18N because of a " "
- override fun onClick(v: View) {
- if (MainActivity.DEBUG) {
- Log.d(TAG, "onQualitySelectorClicked() called")
- }
-
- qualityPopupMenu.show()
- player.isSomePopupMenuVisible = true
-
- val videoStream = player.selectedVideoStream
- if (videoStream != null) {
- player.binding.qualityTextView.text =
- MediaFormat.getNameById(videoStream.formatId) + " " + videoStream.getResolution()
- }
-
- player.saveWasPlaying()
- player.manageControlsAfterOnClick(v)
- }
-}
diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/PlayerMediaSession.java b/app/src/main/java/org/schabi/newpipe/player/playback/PlayerMediaSession.java
index ee0a6f11819..2f261a0fa11 100644
--- a/app/src/main/java/org/schabi/newpipe/player/playback/PlayerMediaSession.java
+++ b/app/src/main/java/org/schabi/newpipe/player/playback/PlayerMediaSession.java
@@ -8,6 +8,9 @@
import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.mediasession.MediaSessionCallback;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
+import org.schabi.newpipe.player.ui.VideoPlayerUi;
+
+import java.util.Optional;
public class PlayerMediaSession implements MediaSessionCallback {
private final Player player;
@@ -89,7 +92,7 @@ public MediaDescriptionCompat getQueueMetadata(final int index) {
public void play() {
player.play();
// hide the player controls even if the play command came from the media session
- player.hideControls(0, 0);
+ player.UIs().get(VideoPlayerUi.class).ifPresent(playerUi -> playerUi.hideControls(0, 0));
}
@Override
diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java
new file mode 100644
index 00000000000..10ed424bab8
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java
@@ -0,0 +1,937 @@
+package org.schabi.newpipe.player.ui;
+
+import static org.schabi.newpipe.MainActivity.DEBUG;
+import static org.schabi.newpipe.QueueItemMenuUtil.openPopupMenu;
+import static org.schabi.newpipe.ktx.ViewUtils.animate;
+import static org.schabi.newpipe.player.Player.STATE_COMPLETED;
+import static org.schabi.newpipe.player.Player.STATE_PAUSED;
+import static org.schabi.newpipe.player.PlayerService.ACTION_PLAY_PAUSE;
+import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND;
+import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE;
+import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_POPUP;
+import static org.schabi.newpipe.player.helper.PlayerHelper.getMinimizeOnExitAction;
+import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString;
+import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientationLocked;
+
+import android.content.Intent;
+import android.content.res.Resources;
+import android.database.ContentObserver;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.os.Build;
+import android.os.Handler;
+import android.provider.Settings;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.content.res.AppCompatResources;
+import androidx.recyclerview.widget.ItemTouchHelper;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.google.android.exoplayer2.video.VideoSize;
+
+import org.schabi.newpipe.R;
+import org.schabi.newpipe.databinding.PlayerBinding;
+import org.schabi.newpipe.extractor.stream.StreamInfo;
+import org.schabi.newpipe.extractor.stream.StreamSegment;
+import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
+import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
+import org.schabi.newpipe.info_list.StreamSegmentAdapter;
+import org.schabi.newpipe.ktx.AnimationType;
+import org.schabi.newpipe.player.Player;
+import org.schabi.newpipe.player.event.PlayerServiceEventListener;
+import org.schabi.newpipe.player.gesture.BasePlayerGestureListener;
+import org.schabi.newpipe.player.gesture.MainPlayerGestureListener;
+import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
+import org.schabi.newpipe.player.helper.PlayerHelper;
+import org.schabi.newpipe.player.playqueue.PlayQueue;
+import org.schabi.newpipe.player.playqueue.PlayQueueAdapter;
+import org.schabi.newpipe.player.playqueue.PlayQueueItem;
+import org.schabi.newpipe.player.playqueue.PlayQueueItemBuilder;
+import org.schabi.newpipe.player.playqueue.PlayQueueItemHolder;
+import org.schabi.newpipe.player.playqueue.PlayQueueItemTouchCallback;
+import org.schabi.newpipe.util.DeviceUtils;
+import org.schabi.newpipe.util.NavigationHelper;
+import org.schabi.newpipe.util.external_communication.KoreUtils;
+
+import java.util.List;
+import java.util.Objects;
+
+public final class MainPlayerUi extends VideoPlayerUi {
+ private static final String TAG = MainPlayerUi.class.getSimpleName();
+
+ private boolean isFullscreen = false;
+ private boolean isVerticalVideo = false;
+ private boolean fragmentIsVisible = false;
+
+ private ContentObserver settingsContentObserver;
+
+ private PlayQueueAdapter playQueueAdapter;
+ private StreamSegmentAdapter segmentAdapter;
+ private boolean isQueueVisible = false;
+ private boolean areSegmentsVisible = false;
+
+ // fullscreen player
+ private ItemTouchHelper itemTouchHelper;
+
+ public MainPlayerUi(@NonNull final Player player,
+ @NonNull final PlayerBinding playerBinding) {
+ super(player, playerBinding);
+ }
+
+ /**
+ * Open fullscreen on tablets where the option to have the main player start automatically in
+ * fullscreen mode is on. Rotating the device to landscape is already done in {@link
+ * VideoDetailFragment#openVideoPlayer(boolean)} when the thumbnail is clicked, and that's
+ * enough for phones, but not for tablets since the mini player can be also shown in landscape.
+ */
+ private void directlyOpenFullscreenIfNeeded() {
+ if (PlayerHelper.isStartMainPlayerFullscreenEnabled(player.getService())
+ && DeviceUtils.isTablet(player.getService())
+ && PlayerHelper.globalScreenOrientationLocked(player.getService())) {
+ player.getFragmentListener().ifPresent(
+ PlayerServiceEventListener::onScreenRotationButtonClicked);
+ }
+ }
+
+ @Override
+ public void setupAfterIntent() {
+ // needed for tablets, check the function for a better explanation
+ directlyOpenFullscreenIfNeeded();
+
+ super.setupAfterIntent();
+
+ binding.getRoot().setVisibility(View.VISIBLE);
+ initVideoPlayer();
+ // Android TV: without it focus will frame the whole player
+ binding.playPauseButton.requestFocus();
+
+ // Note: This is for automatically playing (when "Resume playback" is off), see #6179
+ if (player.getPlayWhenReady()) {
+ player.play();
+ } else {
+ player.pause();
+ }
+ }
+
+ @Override
+ BasePlayerGestureListener buildGestureListener() {
+ return new MainPlayerGestureListener(this);
+ }
+
+ @Override
+ protected void initListeners() {
+ super.initListeners();
+
+ binding.queueButton.setOnClickListener(v -> onQueueClicked());
+ binding.segmentsButton.setOnClickListener(v -> onSegmentsClicked());
+
+ binding.addToPlaylistButton.setOnClickListener(v ->
+ player.onAddToPlaylistClicked(getParentActivity().getSupportFragmentManager()));
+
+ settingsContentObserver = new ContentObserver(new Handler()) {
+ @Override
+ public void onChange(final boolean selfChange) {
+ setupScreenRotationButton();
+ }
+ };
+ context.getContentResolver().registerContentObserver(
+ Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION), false,
+ settingsContentObserver);
+
+ binding.getRoot().addOnLayoutChangeListener(this::onLayoutChange);
+ }
+
+ @Override
+ public void initPlayback() {
+ super.initPlayback();
+
+ if (playQueueAdapter != null) {
+ playQueueAdapter.dispose();
+ }
+ playQueueAdapter = new PlayQueueAdapter(context,
+ Objects.requireNonNull(player.getPlayQueue()));
+ segmentAdapter = new StreamSegmentAdapter(getStreamSegmentListener());
+ }
+
+ @Override
+ public void removeViewFromParent() {
+ // view was added to fragment
+ final ViewParent parent = binding.getRoot().getParent();
+ if (parent instanceof ViewGroup) {
+ ((ViewGroup) parent).removeView(binding.getRoot());
+ }
+ }
+
+ @Override
+ public void destroy() {
+ super.destroy();
+ context.getContentResolver().unregisterContentObserver(settingsContentObserver);
+
+ // Exit from fullscreen when user closes the player via notification
+ if (isFullscreen) {
+ toggleFullscreen();
+ }
+
+ removeViewFromParent();
+ }
+
+ @Override
+ public void destroyPlayer() {
+ super.destroyPlayer();
+
+ if (playQueueAdapter != null) {
+ playQueueAdapter.unsetSelectedListener();
+ playQueueAdapter.dispose();
+ }
+ }
+
+ @Override
+ public void smoothStopForImmediateReusing() {
+ super.smoothStopForImmediateReusing();
+ // Android TV will handle back button in case controls will be visible
+ // (one more additional unneeded click while the player is hidden)
+ hideControls(0, 0);
+ closeItemsList();
+ }
+
+ private void initVideoPlayer() {
+ // restore last resize mode
+ setResizeMode(PlayerHelper.retrieveResizeModeFromPrefs(player));
+ binding.getRoot().setLayoutParams(new FrameLayout.LayoutParams(
+ FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT));
+ }
+
+ @Override
+ protected void setupElementsVisibility() {
+ super.setupElementsVisibility();
+
+ closeItemsList();
+ showHideKodiButton();
+ binding.fullScreenButton.setVisibility(View.GONE);
+ setupScreenRotationButton();
+ binding.resizeTextView.setVisibility(View.VISIBLE);
+ binding.getRoot().findViewById(R.id.metadataView).setVisibility(View.VISIBLE);
+ binding.moreOptionsButton.setVisibility(View.VISIBLE);
+ binding.topControls.setOrientation(LinearLayout.VERTICAL);
+ binding.primaryControls.getLayoutParams().width
+ = LinearLayout.LayoutParams.MATCH_PARENT;
+ binding.secondaryControls.setVisibility(View.INVISIBLE);
+ binding.moreOptionsButton.setImageDrawable(AppCompatResources.getDrawable(context,
+ R.drawable.ic_expand_more));
+ binding.share.setVisibility(View.VISIBLE);
+ binding.openInBrowser.setVisibility(View.VISIBLE);
+ binding.switchMute.setVisibility(View.VISIBLE);
+ binding.playerCloseButton.setVisibility(isFullscreen ? View.GONE : View.VISIBLE);
+ // Top controls have a large minHeight which is allows to drag the player
+ // down in fullscreen mode (just larger area to make easy to locate by finger)
+ binding.topControls.setClickable(true);
+ binding.topControls.setFocusable(true);
+
+ if (isFullscreen) {
+ binding.titleTextView.setVisibility(View.VISIBLE);
+ binding.channelTextView.setVisibility(View.VISIBLE);
+ } else {
+ binding.titleTextView.setVisibility(View.GONE);
+ binding.channelTextView.setVisibility(View.GONE);
+ }
+ }
+
+ @Override
+ protected void setupElementsSize(final Resources resources) {
+ setupElementsSize(
+ resources.getDimensionPixelSize(R.dimen.player_main_buttons_min_width),
+ resources.getDimensionPixelSize(R.dimen.player_main_top_padding),
+ resources.getDimensionPixelSize(R.dimen.player_main_controls_padding),
+ resources.getDimensionPixelSize(R.dimen.player_main_buttons_padding)
+ );
+ }
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Broadcast receiver
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Broadcast receiver
+ @Override
+ public void onBroadcastReceived(final Intent intent) {
+ super.onBroadcastReceived(intent);
+ if (Intent.ACTION_CONFIGURATION_CHANGED.equals(intent.getAction())) {
+ // Close it because when changing orientation from portrait
+ // (in fullscreen mode) the size of queue layout can be larger than the screen size
+ closeItemsList();
+ } else if (ACTION_PLAY_PAUSE.equals(intent.getAction())) {
+ // Ensure that we have audio-only stream playing when a user
+ // started to play from notification's play button from outside of the app
+ if (!fragmentIsVisible) {
+ onFragmentStopped();
+ }
+ } else if (VideoDetailFragment.ACTION_VIDEO_FRAGMENT_STOPPED.equals(intent.getAction())) {
+ fragmentIsVisible = false;
+ onFragmentStopped();
+ } else if (VideoDetailFragment.ACTION_VIDEO_FRAGMENT_RESUMED.equals(intent.getAction())) {
+ // Restore video source when user returns to the fragment
+ fragmentIsVisible = true;
+ player.useVideoSource(true);
+
+ // When a user returns from background, the system UI will always be shown even if
+ // controls are invisible: hide it in that case
+ if (!isControlsVisible()) {
+ hideSystemUIIfNeeded();
+ }
+ }
+ }
+ //endregion
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Fragment binding
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Fragment binding
+ @Override
+ public void onFragmentListenerSet() {
+ super.onFragmentListenerSet();
+ fragmentIsVisible = true;
+ // Apply window insets because Android will not do it when orientation changes
+ // from landscape to portrait
+ if (!isFullscreen) {
+ binding.playbackControlRoot.setPadding(0, 0, 0, 0);
+ }
+ binding.itemsListPanel.setPadding(0, 0, 0, 0);
+ player.getFragmentListener().ifPresent(PlayerServiceEventListener::onViewCreated);
+ }
+
+ /**
+ * This will be called when a user goes to another app/activity, turns off a screen.
+ * We don't want to interrupt playback and don't want to see notification so
+ * next lines of code will enable audio-only playback only if needed
+ */
+ private void onFragmentStopped() {
+ if (player.isPlaying() || player.isLoading()) {
+ switch (getMinimizeOnExitAction(context)) {
+ case MINIMIZE_ON_EXIT_MODE_BACKGROUND:
+ player.useVideoSource(false);
+ break;
+ case MINIMIZE_ON_EXIT_MODE_POPUP:
+ player.setRecovery();
+ NavigationHelper.playOnPopupPlayer(getParentActivity(),
+ player.getPlayQueue(), true);
+ break;
+ case MINIMIZE_ON_EXIT_MODE_NONE: default:
+ player.pause();
+ break;
+ }
+ }
+ }
+ //endregion
+
+ private void showHideKodiButton() {
+ // show kodi button if it supports the current service and it is enabled in settings
+ @Nullable final PlayQueue playQueue = player.getPlayQueue();
+ binding.playWithKodi.setVisibility(playQueue != null && playQueue.getItem() != null
+ && KoreUtils.shouldShowPlayWithKodi(context, playQueue.getItem().getServiceId())
+ ? View.VISIBLE : View.GONE);
+ }
+
+ @Override
+ public void onUpdateProgress(final int currentProgress,
+ final int duration,
+ final int bufferPercent) {
+ super.onUpdateProgress(currentProgress, duration, bufferPercent);
+
+ if (areSegmentsVisible) {
+ segmentAdapter.selectSegmentAt(getNearestStreamSegmentPosition(currentProgress));
+ }
+ if (isQueueVisible) {
+ updateQueueTime(currentProgress);
+ }
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Controls showing / hiding
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Controls showing / hiding
+
+ protected void showOrHideButtons() {
+ super.showOrHideButtons();
+ @Nullable final PlayQueue playQueue = player.getPlayQueue();
+ if (playQueue == null) {
+ return;
+ }
+
+ final boolean showQueue = playQueue.getStreams().size() > 1;
+ final boolean showSegment = !player.getCurrentStreamInfo()
+ .map(StreamInfo::getStreamSegments)
+ .map(List::isEmpty)
+ .orElse(/*no stream info=*/true);
+
+ binding.queueButton.setVisibility(showQueue ? View.VISIBLE : View.GONE);
+ binding.queueButton.setAlpha(showQueue ? 1.0f : 0.0f);
+ binding.segmentsButton.setVisibility(showSegment ? View.VISIBLE : View.GONE);
+ binding.segmentsButton.setAlpha(showSegment ? 1.0f : 0.0f);
+ }
+
+ @Override
+ public void showSystemUIPartially() {
+ if (isFullscreen) {
+ final Window window = getParentActivity().getWindow();
+ window.setStatusBarColor(Color.TRANSPARENT);
+ window.setNavigationBarColor(Color.TRANSPARENT);
+ final int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+ | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
+ window.getDecorView().setSystemUiVisibility(visibility);
+ window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
+ }
+ }
+
+ @Override
+ public void hideSystemUIIfNeeded() {
+ player.getFragmentListener().ifPresent(PlayerServiceEventListener::hideSystemUiIfNeeded);
+ }
+
+ /**
+ * Calculate the maximum allowed height for the {@link R.id.endScreen}
+ * to prevent it from enlarging the player.
+ *
+ * The calculating follows these rules:
+ *
+ *
+ * Show at least stream title and content creator on TVs and tablets
+ * when in landscape (always the case for TVs) and not in fullscreen mode.
+ * This requires to have at least 85dp free space for {@link R.id.detail_root}
+ * and additional space for the stream title text size
+ * ({@link R.id.detail_title_root_layout}).
+ * The text size is 15sp on tablets and 16sp on TVs,
+ * see {@link R.id.titleTextView}.
+ *
+ *
+ * Otherwise, the max thumbnail height is the screen height.
+ * TODO investigate why this is done on popup player, too
+ *
+ *
+ *
+ * @param bitmap the bitmap that needs to be resized to fit the end screen
+ * @return the maximum height for the end screen thumbnail
+ */
+ @Override
+ protected float calculateMaxEndScreenThumbnailHeight(@NonNull final Bitmap bitmap) {
+ final int screenHeight = context.getResources().getDisplayMetrics().heightPixels;
+
+ if (DeviceUtils.isTv(context) && !isFullscreen()) {
+ final int videoInfoHeight =
+ DeviceUtils.dpToPx(85, context) + DeviceUtils.spToPx(16, context);
+ return Math.min(bitmap.getHeight(), screenHeight - videoInfoHeight);
+ } else if (DeviceUtils.isTablet(context) && isLandscape() && !isFullscreen()) {
+ final int videoInfoHeight =
+ DeviceUtils.dpToPx(85, context) + DeviceUtils.spToPx(15, context);
+ return Math.min(bitmap.getHeight(), screenHeight - videoInfoHeight);
+ } else { // fullscreen player: max height is the device height
+ return Math.min(bitmap.getHeight(), screenHeight);
+ }
+ }
+ //endregion
+
+ @Override
+ public void onPlaying() {
+ super.onPlaying();
+ checkLandscape();
+ }
+
+ @Override
+ public void onCompleted() {
+ super.onCompleted();
+ if (isFullscreen) {
+ toggleFullscreen();
+ }
+ }
+
+
+ @Override
+ protected void setupSubtitleView(float captionScale) {
+ final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
+ final int minimumLength = Math.min(metrics.heightPixels, metrics.widthPixels);
+ final float captionRatioInverse = 20f + 4f * (1.0f - captionScale);
+ binding.subtitleView.setFixedTextSize(
+ TypedValue.COMPLEX_UNIT_PX, minimumLength / captionRatioInverse);
+ }
+
+
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Gestures
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Gestures
+
+ @SuppressWarnings("checkstyle:ParameterNumber")
+ private void onLayoutChange(final View view, final int l, final int t, final int r, final int b,
+ final int ol, final int ot, final int or, final int ob) {
+ if (l != ol || t != ot || r != or || b != ob) {
+ // Use smaller value to be consistent between screen orientations
+ // (and to make usage easier)
+ final int width = r - l;
+ final int height = b - t;
+ final int min = Math.min(width, height);
+ final int maxGestureLength = (int) (min * MainPlayerGestureListener.MAX_GESTURE_LENGTH);
+
+ if (DEBUG) {
+ Log.d(TAG, "maxGestureLength = " + maxGestureLength);
+ }
+
+ binding.volumeProgressBar.setMax(maxGestureLength);
+ binding.brightnessProgressBar.setMax(maxGestureLength);
+
+ setInitialGestureValues();
+ binding.itemsListPanel.getLayoutParams().height
+ = height - binding.itemsListPanel.getTop();
+ }
+ }
+
+ private void setInitialGestureValues() {
+ if (player.getAudioReactor() != null) {
+ final float currentVolumeNormalized =
+ (float) player.getAudioReactor().getVolume()
+ / player.getAudioReactor().getMaxVolume();
+ binding.volumeProgressBar.setProgress(
+ (int) (binding.volumeProgressBar.getMax() * currentVolumeNormalized));
+ }
+ }
+ //endregion
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Play queue, segments and streams
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Play queue, segments and streams
+
+ @Override
+ public void onMetadataChanged(@NonNull final StreamInfo info) {
+ super.onMetadataChanged(info);
+ showHideKodiButton();
+ if (areSegmentsVisible) {
+ if (segmentAdapter.setItems(info)) {
+ final int adapterPosition = getNearestStreamSegmentPosition(
+ player.getExoPlayer().getCurrentPosition());
+ segmentAdapter.selectSegmentAt(adapterPosition);
+ binding.itemsList.scrollToPosition(adapterPosition);
+ } else {
+ closeItemsList();
+ }
+ }
+ }
+
+ @Override
+ public void onPlayQueueEdited() {
+ super.onPlayQueueEdited();
+ showOrHideButtons();
+ }
+
+ private void onQueueClicked() {
+ isQueueVisible = true;
+
+ hideSystemUIIfNeeded();
+ buildQueue();
+
+ binding.itemsListHeaderTitle.setVisibility(View.GONE);
+ binding.itemsListHeaderDuration.setVisibility(View.VISIBLE);
+ binding.shuffleButton.setVisibility(View.VISIBLE);
+ binding.repeatButton.setVisibility(View.VISIBLE);
+ binding.addToPlaylistButton.setVisibility(View.VISIBLE);
+
+ hideControls(0, 0);
+ binding.itemsListPanel.requestFocus();
+ animate(binding.itemsListPanel, true, DEFAULT_CONTROLS_DURATION,
+ AnimationType.SLIDE_AND_ALPHA);
+
+ @Nullable final PlayQueue playQueue = player.getPlayQueue();
+ if (playQueue != null) {
+ binding.itemsList.scrollToPosition(playQueue.getIndex());
+ }
+
+ updateQueueTime((int) player.getExoPlayer().getCurrentPosition());
+ }
+
+ private void buildQueue() {
+ binding.itemsList.setAdapter(playQueueAdapter);
+ binding.itemsList.setClickable(true);
+ binding.itemsList.setLongClickable(true);
+
+ binding.itemsList.clearOnScrollListeners();
+ binding.itemsList.addOnScrollListener(getQueueScrollListener());
+
+ itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
+ itemTouchHelper.attachToRecyclerView(binding.itemsList);
+
+ playQueueAdapter.setSelectedListener(getOnSelectedListener());
+
+ binding.itemsListClose.setOnClickListener(view -> closeItemsList());
+ }
+
+ private void onSegmentsClicked() {
+ areSegmentsVisible = true;
+
+ hideSystemUIIfNeeded();
+ buildSegments();
+
+ binding.itemsListHeaderTitle.setVisibility(View.VISIBLE);
+ binding.itemsListHeaderDuration.setVisibility(View.GONE);
+ binding.shuffleButton.setVisibility(View.GONE);
+ binding.repeatButton.setVisibility(View.GONE);
+ binding.addToPlaylistButton.setVisibility(View.GONE);
+
+ hideControls(0, 0);
+ binding.itemsListPanel.requestFocus();
+ animate(binding.itemsListPanel, true, DEFAULT_CONTROLS_DURATION,
+ AnimationType.SLIDE_AND_ALPHA);
+
+ final int adapterPosition = getNearestStreamSegmentPosition(
+ player.getExoPlayer().getCurrentPosition());
+ segmentAdapter.selectSegmentAt(adapterPosition);
+ binding.itemsList.scrollToPosition(adapterPosition);
+ }
+
+ private void buildSegments() {
+ binding.itemsList.setAdapter(segmentAdapter);
+ binding.itemsList.setClickable(true);
+ binding.itemsList.setLongClickable(false);
+
+ binding.itemsList.clearOnScrollListeners();
+ if (itemTouchHelper != null) {
+ itemTouchHelper.attachToRecyclerView(null);
+ }
+
+ player.getCurrentStreamInfo().ifPresent(segmentAdapter::setItems);
+
+ binding.shuffleButton.setVisibility(View.GONE);
+ binding.repeatButton.setVisibility(View.GONE);
+ binding.addToPlaylistButton.setVisibility(View.GONE);
+ binding.itemsListClose.setOnClickListener(view -> closeItemsList());
+ }
+
+ public void closeItemsList() {
+ if (isQueueVisible || areSegmentsVisible) {
+ isQueueVisible = false;
+ areSegmentsVisible = false;
+
+ if (itemTouchHelper != null) {
+ itemTouchHelper.attachToRecyclerView(null);
+ }
+
+ animate(binding.itemsListPanel, false, DEFAULT_CONTROLS_DURATION,
+ AnimationType.SLIDE_AND_ALPHA, 0, () -> {
+ // Even when queueLayout is GONE it receives touch events
+ // and ruins normal behavior of the app. This line fixes it
+ binding.itemsListPanel.setTranslationY(
+ -binding.itemsListPanel.getHeight() * 5);
+ });
+
+ // clear focus, otherwise a white rectangle remains on top of the player
+ binding.itemsListClose.clearFocus();
+ binding.playPauseButton.requestFocus();
+ }
+ }
+
+ private OnScrollBelowItemsListener getQueueScrollListener() {
+ return new OnScrollBelowItemsListener() {
+ @Override
+ public void onScrolledDown(final RecyclerView recyclerView) {
+ @Nullable final PlayQueue playQueue = player.getPlayQueue();
+ if (playQueue != null && !playQueue.isComplete()) {
+ playQueue.fetch();
+ } else if (binding != null) {
+ binding.itemsList.clearOnScrollListeners();
+ }
+ }
+ };
+ }
+
+ private StreamSegmentAdapter.StreamSegmentListener getStreamSegmentListener() {
+ return (item, seconds) -> {
+ segmentAdapter.selectSegment(item);
+ player.seekTo(seconds * 1000L);
+ player.triggerProgressUpdate();
+ };
+ }
+
+ private int getNearestStreamSegmentPosition(final long playbackPosition) {
+ //noinspection SimplifyOptionalCallChains
+ if (!player.getCurrentStreamInfo().isPresent()) {
+ return 0;
+ }
+
+ int nearestPosition = 0;
+ final List segments
+ = player.getCurrentStreamInfo().get().getStreamSegments();
+
+ for (int i = 0; i < segments.size(); i++) {
+ if (segments.get(i).getStartTimeSeconds() * 1000L > playbackPosition) {
+ break;
+ }
+ nearestPosition++;
+ }
+ return Math.max(0, nearestPosition - 1);
+ }
+
+ private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
+ return new PlayQueueItemTouchCallback() {
+ @Override
+ public void onMove(final int sourceIndex, final int targetIndex) {
+ @Nullable final PlayQueue playQueue = player.getPlayQueue();
+ if (playQueue != null) {
+ playQueue.move(sourceIndex, targetIndex);
+ }
+ }
+
+ @Override
+ public void onSwiped(final int index) {
+ @Nullable final PlayQueue playQueue = player.getPlayQueue();
+ if (playQueue != null && index != -1) {
+ playQueue.remove(index);
+ }
+ }
+ };
+ }
+
+ private PlayQueueItemBuilder.OnSelectedListener getOnSelectedListener() {
+ return new PlayQueueItemBuilder.OnSelectedListener() {
+ @Override
+ public void selected(final PlayQueueItem item, final View view) {
+ player.selectQueueItem(item);
+ }
+
+ @Override
+ public void held(final PlayQueueItem item, final View view) {
+ @Nullable final PlayQueue playQueue = player.getPlayQueue();
+ @Nullable final AppCompatActivity parentActivity = getParentActivity();
+ if (playQueue != null && parentActivity != null && playQueue.indexOf(item) != -1) {
+ openPopupMenu(player.getPlayQueue(), item, view, true,
+ parentActivity.getSupportFragmentManager(), context);
+ }
+ }
+
+ @Override
+ public void onStartDrag(final PlayQueueItemHolder viewHolder) {
+ if (itemTouchHelper != null) {
+ itemTouchHelper.startDrag(viewHolder);
+ }
+ }
+ };
+ }
+
+ private void updateQueueTime(final int currentTime) {
+ @Nullable final PlayQueue playQueue = player.getPlayQueue();
+ if (playQueue == null) {
+ return;
+ }
+
+ final int currentStream = playQueue.getIndex();
+ int before = 0;
+ int after = 0;
+
+ final List streams = playQueue.getStreams();
+ final int nStreams = streams.size();
+
+ for (int i = 0; i < nStreams; i++) {
+ if (i < currentStream) {
+ before += streams.get(i).getDuration();
+ } else {
+ after += streams.get(i).getDuration();
+ }
+ }
+
+ before *= 1000;
+ after *= 1000;
+
+ binding.itemsListHeaderDuration.setText(
+ String.format("%s/%s",
+ getTimeString(currentTime + before),
+ getTimeString(before + after)
+ ));
+ }
+
+ @Override
+ protected boolean isAnyListViewOpen() {
+ return isQueueVisible || areSegmentsVisible;
+ }
+
+ @Override
+ public boolean isFullscreen() {
+ return isFullscreen;
+ }
+
+ public boolean isVerticalVideo() {
+ return isVerticalVideo;
+ }
+
+ //endregion
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Click listeners
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Click listeners
+ @Override
+ public void onClick(final View v) {
+ if (v.getId() == binding.screenRotationButton.getId()) {
+ // Only if it's not a vertical video or vertical video but in landscape with locked
+ // orientation a screen orientation can be changed automatically
+ if (!isVerticalVideo || (isLandscape() && globalScreenOrientationLocked(context))) {
+ player.getFragmentListener().ifPresent(
+ PlayerServiceEventListener::onScreenRotationButtonClicked);
+ } else {
+ toggleFullscreen();
+ }
+ }
+
+ // call it later since it calls manageControlsAfterOnClick at the end
+ super.onClick(v);
+ }
+
+ @Override
+ protected void onPlaybackSpeedClicked() {
+ PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), player.getPlaybackPitch(),
+ player.getPlaybackSkipSilence(), (speed, pitch, skipSilence)
+ -> player.setPlaybackParameters(speed, pitch, skipSilence))
+ .show(getParentActivity().getSupportFragmentManager(), null);
+ }
+
+ @Override
+ public boolean onLongClick(final View v) {
+ if (v.getId() == binding.moreOptionsButton.getId() && isFullscreen) {
+ player.getFragmentListener().ifPresent(
+ PlayerServiceEventListener::onMoreOptionsLongClicked);
+ hideControls(0, 0);
+ hideSystemUIIfNeeded();
+ return true;
+ }
+ return super.onLongClick(v);
+ }
+
+ @Override
+ public boolean onKeyDown(final int keyCode) {
+ if (keyCode == KeyEvent.KEYCODE_SPACE && isFullscreen) {
+ player.playPause();
+ if (player.isPlaying()) {
+ hideControls(0, 0);
+ }
+ return true;
+ }
+ return super.onKeyDown(keyCode);
+ }
+ //endregion
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Video size, resize, orientation, fullscreen
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Video size, resize, orientation, fullscreen
+
+ private void setupScreenRotationButton() {
+ binding.screenRotationButton.setVisibility(globalScreenOrientationLocked(context)
+ || isVerticalVideo || DeviceUtils.isTablet(context)
+ ? View.VISIBLE : View.GONE);
+ binding.screenRotationButton.setImageDrawable(AppCompatResources.getDrawable(context,
+ isFullscreen ? R.drawable.ic_fullscreen_exit
+ : R.drawable.ic_fullscreen));
+ }
+
+ @Override
+ public void onVideoSizeChanged(@NonNull final VideoSize videoSize) {
+ super.onVideoSizeChanged(videoSize);
+ isVerticalVideo = videoSize.width < videoSize.height;
+
+ if (globalScreenOrientationLocked(context)
+ && isFullscreen
+ && isLandscape() == isVerticalVideo
+ && !DeviceUtils.isTv(context)
+ && !DeviceUtils.isTablet(context)) {
+ // set correct orientation
+ player.getFragmentListener().ifPresent(
+ PlayerServiceEventListener::onScreenRotationButtonClicked);
+ }
+
+ setupScreenRotationButton();
+ }
+
+ public void toggleFullscreen() {
+ if (DEBUG) {
+ Log.d(TAG, "toggleFullscreen() called");
+ }
+ final PlayerServiceEventListener fragmentListener
+ = player.getFragmentListener().orElse(null);
+ if (fragmentListener == null || player.exoPlayerIsNull()) {
+ return;
+ }
+
+ isFullscreen = !isFullscreen;
+ if (!isFullscreen) {
+ // Apply window insets because Android will not do it when orientation changes
+ // from landscape to portrait (open vertical video to reproduce)
+ binding.playbackControlRoot.setPadding(0, 0, 0, 0);
+ } else {
+ // Android needs tens milliseconds to send new insets but a user is able to see
+ // how controls changes it's position from `0` to `nav bar height` padding.
+ // So just hide the controls to hide this visual inconsistency
+ hideControls(0, 0);
+ }
+ fragmentListener.onFullscreenStateChanged(isFullscreen);
+
+ if (isFullscreen) {
+ binding.titleTextView.setVisibility(View.VISIBLE);
+ binding.channelTextView.setVisibility(View.VISIBLE);
+ binding.playerCloseButton.setVisibility(View.GONE);
+ } else {
+ binding.titleTextView.setVisibility(View.GONE);
+ binding.channelTextView.setVisibility(View.GONE);
+ binding.playerCloseButton.setVisibility(View.VISIBLE);
+ }
+ setupScreenRotationButton();
+ }
+
+ public void checkLandscape() {
+ // check if landscape is correct
+ final boolean videoInLandscapeButNotInFullscreen
+ = isLandscape() && !isFullscreen && !player.isAudioOnly();
+ final boolean notPaused = player.getCurrentState() != STATE_COMPLETED
+ && player.getCurrentState() != STATE_PAUSED;
+
+ if (videoInLandscapeButNotInFullscreen
+ && notPaused
+ && !DeviceUtils.isTablet(context)) {
+ toggleFullscreen();
+ }
+ }
+ //endregion
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Getters
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Getters
+ public PlayerBinding getBinding() {
+ return binding;
+ }
+
+ public AppCompatActivity getParentActivity() {
+ return (AppCompatActivity) ((ViewGroup) binding.getRoot().getParent()).getContext();
+ }
+
+ public boolean isLandscape() {
+ // DisplayMetrics from activity context knows about MultiWindow feature
+ // while DisplayMetrics from app context doesn't
+ return DeviceUtils.isLandscape(getParentActivity());
+ }
+ //endregion
+}
diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/NotificationPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/NotificationPlayerUi.java
new file mode 100644
index 00000000000..40c83c6c779
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/ui/NotificationPlayerUi.java
@@ -0,0 +1,26 @@
+package org.schabi.newpipe.player.ui;
+
+import androidx.annotation.NonNull;
+
+import org.schabi.newpipe.player.NotificationUtil;
+import org.schabi.newpipe.player.Player;
+
+public final class NotificationPlayerUi extends PlayerUi {
+ boolean foregroundNotificationAlreadyCreated = false;
+
+ public NotificationPlayerUi(@NonNull final Player player) {
+ super(player);
+ }
+
+ @Override
+ public void initPlayer() {
+ super.initPlayer();
+ if (!foregroundNotificationAlreadyCreated) {
+ NotificationUtil.getInstance()
+ .createNotificationAndStartForeground(player, player.getService());
+ foregroundNotificationAlreadyCreated = true;
+ }
+ }
+
+ // TODO TODO on destroy remove foreground
+}
diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java
new file mode 100644
index 00000000000..fd63790d6f7
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java
@@ -0,0 +1,120 @@
+package org.schabi.newpipe.player.ui;
+
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.google.android.exoplayer2.PlaybackParameters;
+import com.google.android.exoplayer2.Player.RepeatMode;
+import com.google.android.exoplayer2.Tracks;
+import com.google.android.exoplayer2.text.Cue;
+import com.google.android.exoplayer2.video.VideoSize;
+
+import org.schabi.newpipe.extractor.stream.StreamInfo;
+import org.schabi.newpipe.player.Player;
+
+import java.util.List;
+
+public abstract class PlayerUi {
+ private static final String TAG = PlayerUi.class.getSimpleName();
+
+ @NonNull protected Context context;
+ @NonNull protected Player player;
+
+ public PlayerUi(@NonNull final Player player) {
+ this.context = player.getContext();
+ this.player = player;
+ }
+
+ @NonNull
+ public Player getPlayer() {
+ return player;
+ }
+
+
+ public void setupAfterIntent() {
+ }
+
+ public void initPlayer() {
+ }
+
+ public void initPlayback() {
+ }
+
+ public void destroyPlayer() {
+ }
+
+ public void destroy() {
+ }
+
+ public void smoothStopForImmediateReusing() {
+ }
+
+ public void onFragmentListenerSet() {
+ }
+
+ public void onBroadcastReceived(final Intent intent) {
+ }
+
+ public void onUpdateProgress(final int currentProgress,
+ final int duration,
+ final int bufferPercent) {
+ }
+
+ public void onPrepared() {
+ }
+
+ public void onBlocked() {
+ }
+
+ public void onPlaying() {
+ }
+
+ public void onBuffering() {
+ }
+
+ public void onPaused() {
+ }
+
+ public void onPausedSeek() {
+ }
+
+ public void onCompleted() {
+ }
+
+ public void onRepeatModeChanged(@RepeatMode final int repeatMode) {
+ }
+
+ public void onShuffleModeEnabledChanged(final boolean shuffleModeEnabled) {
+ }
+
+ public void onMuteUnmuteChanged(final boolean isMuted) {
+ }
+
+ public void onTextTracksChanged(@NonNull final Tracks currentTracks) {
+ }
+
+ public void onPlaybackParametersChanged(@NonNull final PlaybackParameters playbackParameters) {
+ }
+
+ public void onRenderedFirstFrame() {
+ }
+
+ public void onCues(@NonNull final List cues) {
+ }
+
+ public void onMetadataChanged(@NonNull final StreamInfo info) {
+ }
+
+ public void onThumbnailLoaded(@Nullable final Bitmap bitmap) {
+ }
+
+ public void onPlayQueueEdited() {
+ }
+
+ public void onVideoSizeChanged(@NonNull final VideoSize videoSize) {
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUiList.java b/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUiList.java
new file mode 100644
index 00000000000..8c5c0dbfabd
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUiList.java
@@ -0,0 +1,36 @@
+package org.schabi.newpipe.player.ui;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Consumer;
+
+public final class PlayerUiList {
+ final List playerUis = new ArrayList<>();
+
+ public void add(final PlayerUi playerUi) {
+ playerUis.add(playerUi);
+ }
+
+ public void destroyAll(final Class playerUiType) {
+ playerUis.stream()
+ .filter(playerUiType::isInstance)
+ .forEach(playerUi -> {
+ playerUi.destroyPlayer();
+ playerUi.destroy();
+ });
+ playerUis.removeIf(playerUiType::isInstance);
+ }
+
+ public Optional get(final Class playerUiType) {
+ return playerUis.stream()
+ .filter(playerUiType::isInstance)
+ .map(playerUiType::cast)
+ .findFirst();
+ }
+
+ public void call(final Consumer consumer) {
+ //noinspection SimplifyStreamApiCallChains
+ playerUis.stream().forEach(consumer);
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/PopupPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/PopupPlayerUi.java
new file mode 100644
index 00000000000..b8a26a23314
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/ui/PopupPlayerUi.java
@@ -0,0 +1,460 @@
+package org.schabi.newpipe.player.ui;
+
+import static org.schabi.newpipe.MainActivity.DEBUG;
+import static org.schabi.newpipe.player.helper.PlayerHelper.buildCloseOverlayLayoutParams;
+import static org.schabi.newpipe.player.helper.PlayerHelper.getMinimumVideoHeight;
+import static org.schabi.newpipe.player.helper.PlayerHelper.retrievePopupLayoutParamsFromPrefs;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.animation.AnticipateInterpolator;
+import android.widget.LinearLayout;
+
+import androidx.annotation.NonNull;
+import androidx.core.content.ContextCompat;
+
+import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
+import com.google.android.exoplayer2.ui.SubtitleView;
+
+import org.schabi.newpipe.R;
+import org.schabi.newpipe.databinding.PlayerBinding;
+import org.schabi.newpipe.databinding.PlayerPopupCloseOverlayBinding;
+import org.schabi.newpipe.player.Player;
+import org.schabi.newpipe.player.gesture.BasePlayerGestureListener;
+import org.schabi.newpipe.player.gesture.PopupPlayerGestureListener;
+import org.schabi.newpipe.player.helper.PlayerHelper;
+
+public final class PopupPlayerUi extends VideoPlayerUi {
+ private static final String TAG = PopupPlayerUi.class.getSimpleName();
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Popup player
+ //////////////////////////////////////////////////////////////////////////*/
+
+ private PlayerPopupCloseOverlayBinding closeOverlayBinding;
+
+ private boolean isPopupClosing = false;
+
+ private int screenWidth;
+ private int screenHeight;
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Popup player window manager
+ //////////////////////////////////////////////////////////////////////////*/
+
+ public static final int IDLE_WINDOW_FLAGS = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+ | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
+ public static final int ONGOING_PLAYBACK_WINDOW_FLAGS = IDLE_WINDOW_FLAGS
+ | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
+
+ private WindowManager.LayoutParams popupLayoutParams; // null if player is not popup
+ private final WindowManager windowManager;
+
+ public PopupPlayerUi(@NonNull final Player player,
+ @NonNull final PlayerBinding playerBinding) {
+ super(player, playerBinding);
+ windowManager = ContextCompat.getSystemService(context, WindowManager.class);
+ }
+
+ @Override
+ public void setupAfterIntent() {
+ setupElementsVisibility();
+ binding.getRoot().setVisibility(View.VISIBLE);
+ initPopup();
+ initPopupCloseOverlay();
+ binding.playPauseButton.requestFocus();
+ }
+
+ @Override
+ BasePlayerGestureListener buildGestureListener() {
+ return new PopupPlayerGestureListener(this);
+ }
+
+ @SuppressLint("RtlHardcoded")
+ private void initPopup() {
+ if (DEBUG) {
+ Log.d(TAG, "initPopup() called");
+ }
+
+ // Popup is already added to windowManager
+ if (popupHasParent()) {
+ return;
+ }
+
+ updateScreenSize();
+
+ popupLayoutParams = retrievePopupLayoutParamsFromPrefs(this);
+ binding.surfaceView.setHeights(popupLayoutParams.height, popupLayoutParams.height);
+
+ checkPopupPositionBounds();
+
+ binding.loadingPanel.setMinimumWidth(popupLayoutParams.width);
+ binding.loadingPanel.setMinimumHeight(popupLayoutParams.height);
+
+ windowManager.addView(binding.getRoot(), popupLayoutParams);
+
+ // Popup doesn't have aspectRatio selector, using FIT automatically
+ setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIT);
+ }
+
+ @SuppressLint("RtlHardcoded")
+ private void initPopupCloseOverlay() {
+ if (DEBUG) {
+ Log.d(TAG, "initPopupCloseOverlay() called");
+ }
+
+ // closeOverlayView is already added to windowManager
+ if (closeOverlayBinding != null) {
+ return;
+ }
+
+ closeOverlayBinding = PlayerPopupCloseOverlayBinding.inflate(LayoutInflater.from(context));
+
+ final WindowManager.LayoutParams closeOverlayLayoutParams = buildCloseOverlayLayoutParams();
+ closeOverlayBinding.closeButton.setVisibility(View.GONE);
+ windowManager.addView(closeOverlayBinding.getRoot(), closeOverlayLayoutParams);
+ }
+
+ @Override
+ protected void setupElementsVisibility() {
+ binding.fullScreenButton.setVisibility(View.VISIBLE);
+ binding.screenRotationButton.setVisibility(View.GONE);
+ binding.resizeTextView.setVisibility(View.GONE);
+ binding.getRoot().findViewById(R.id.metadataView).setVisibility(View.GONE);
+ binding.queueButton.setVisibility(View.GONE);
+ binding.segmentsButton.setVisibility(View.GONE);
+ binding.moreOptionsButton.setVisibility(View.GONE);
+ binding.topControls.setOrientation(LinearLayout.HORIZONTAL);
+ binding.primaryControls.getLayoutParams().width
+ = LinearLayout.LayoutParams.WRAP_CONTENT;
+ binding.secondaryControls.setAlpha(1.0f);
+ binding.secondaryControls.setVisibility(View.VISIBLE);
+ binding.secondaryControls.setTranslationY(0);
+ binding.share.setVisibility(View.GONE);
+ binding.playWithKodi.setVisibility(View.GONE);
+ binding.openInBrowser.setVisibility(View.GONE);
+ binding.switchMute.setVisibility(View.GONE);
+ binding.playerCloseButton.setVisibility(View.GONE);
+ binding.topControls.bringToFront();
+ binding.topControls.setClickable(false);
+ binding.topControls.setFocusable(false);
+ binding.bottomControls.bringToFront();
+ super.setupElementsVisibility();
+ }
+
+ @Override
+ protected void setupElementsSize(final Resources resources) {
+ setupElementsSize(
+ 0,
+ 0,
+ resources.getDimensionPixelSize(R.dimen.player_popup_controls_padding),
+ resources.getDimensionPixelSize(R.dimen.player_popup_buttons_padding)
+ );
+ }
+
+ @Override
+ public void removeViewFromParent() {
+ // view was added by windowManager for popup player
+ windowManager.removeViewImmediate(binding.getRoot());
+ }
+
+ @Override
+ public void destroy() {
+ super.destroy();
+ removePopupFromView();
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Broadcast receiver
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Broadcast receiver
+ @Override
+ public void onBroadcastReceived(final Intent intent) {
+ super.onBroadcastReceived(intent);
+ if (Intent.ACTION_CONFIGURATION_CHANGED.equals(intent.getAction())) {
+ updateScreenSize();
+ changePopupSize(popupLayoutParams.width);
+ checkPopupPositionBounds();
+ } else if (Intent.ACTION_SCREEN_OFF.equals(intent.getAction())) {
+ // Use only audio source when screen turns off while popup player is playing
+ if (player.isPlaying() || player.isLoading()) {
+ player.useVideoSource(false);
+ }
+ } else if (Intent.ACTION_SCREEN_ON.equals(intent.getAction())) {
+ // Restore video source when screen turns on and user is watching video in popup player
+ if (player.isPlaying() || player.isLoading()) {
+ player.useVideoSource(true);
+ }
+ }
+ }
+ //endregion
+
+
+ /**
+ * Check if {@link #popupLayoutParams}' position is within a arbitrary boundary
+ * that goes from (0, 0) to (screenWidth, screenHeight).
+ *
+ * If it's out of these boundaries, {@link #popupLayoutParams}' position is changed
+ * and {@code true} is returned to represent this change.
+ *
+ */
+ public void checkPopupPositionBounds() {
+ if (DEBUG) {
+ Log.d(TAG, "checkPopupPositionBounds() called with: "
+ + "screenWidth = [" + screenWidth + "], "
+ + "screenHeight = [" + screenHeight + "]");
+ }
+ if (popupLayoutParams == null) {
+ return;
+ }
+
+ if (popupLayoutParams.x < 0) {
+ popupLayoutParams.x = 0;
+ } else if (popupLayoutParams.x > screenWidth - popupLayoutParams.width) {
+ popupLayoutParams.x = screenWidth - popupLayoutParams.width;
+ }
+
+ if (popupLayoutParams.y < 0) {
+ popupLayoutParams.y = 0;
+ } else if (popupLayoutParams.y > screenHeight - popupLayoutParams.height) {
+ popupLayoutParams.y = screenHeight - popupLayoutParams.height;
+ }
+ }
+
+ public void updateScreenSize() {
+ final DisplayMetrics metrics = new DisplayMetrics();
+ windowManager.getDefaultDisplay().getMetrics(metrics);
+
+ screenWidth = metrics.widthPixels;
+ screenHeight = metrics.heightPixels;
+ if (DEBUG) {
+ Log.d(TAG, "updateScreenSize() called: screenWidth = ["
+ + screenWidth + "], screenHeight = [" + screenHeight + "]");
+ }
+ }
+
+ /**
+ * Changes the size of the popup based on the width.
+ * @param width the new width, height is calculated with
+ * {@link PlayerHelper#getMinimumVideoHeight(float)}
+ */
+ public void changePopupSize(final int width) {
+ if (DEBUG) {
+ Log.d(TAG, "changePopupSize() called with: width = [" + width + "]");
+ }
+
+ if (anyPopupViewIsNull()) {
+ return;
+ }
+
+ final float minimumWidth = context.getResources().getDimension(R.dimen.popup_minimum_width);
+ final int actualWidth = (int) (width > screenWidth ? screenWidth
+ : (width < minimumWidth ? minimumWidth : width));
+ final int actualHeight = (int) getMinimumVideoHeight(width);
+ if (DEBUG) {
+ Log.d(TAG, "updatePopupSize() updated values:"
+ + " width = [" + actualWidth + "], height = [" + actualHeight + "]");
+ }
+
+ popupLayoutParams.width = actualWidth;
+ popupLayoutParams.height = actualHeight;
+ binding.surfaceView.setHeights(popupLayoutParams.height, popupLayoutParams.height);
+ windowManager.updateViewLayout(binding.getRoot(), popupLayoutParams);
+ }
+
+ private void changePopupWindowFlags(final int flags) {
+ if (DEBUG) {
+ Log.d(TAG, "changePopupWindowFlags() called with: flags = [" + flags + "]");
+ }
+
+ if (!anyPopupViewIsNull()) {
+ popupLayoutParams.flags = flags;
+ windowManager.updateViewLayout(binding.getRoot(), popupLayoutParams);
+ }
+ }
+
+ public void closePopup() {
+ if (DEBUG) {
+ Log.d(TAG, "closePopup() called, isPopupClosing = " + isPopupClosing);
+ }
+ if (isPopupClosing) {
+ return;
+ }
+ isPopupClosing = true;
+
+ player.saveStreamProgressState();
+ windowManager.removeView(binding.getRoot());
+
+ animatePopupOverlayAndFinishService();
+ }
+
+ public boolean isPopupClosing() {
+ return isPopupClosing;
+ }
+
+ public void removePopupFromView() {
+ if (windowManager != null) {
+ // wrap in try-catch since it could sometimes generate errors randomly
+ try {
+ if (popupHasParent()) {
+ windowManager.removeView(binding.getRoot());
+ }
+ } catch (final IllegalArgumentException e) {
+ Log.w(TAG, "Failed to remove popup from window manager", e);
+ }
+
+ try {
+ final boolean closeOverlayHasParent = closeOverlayBinding != null
+ && closeOverlayBinding.getRoot().getParent() != null;
+ if (closeOverlayHasParent) {
+ windowManager.removeView(closeOverlayBinding.getRoot());
+ }
+ } catch (final IllegalArgumentException e) {
+ Log.w(TAG, "Failed to remove popup overlay from window manager", e);
+ }
+ }
+ }
+
+ private void animatePopupOverlayAndFinishService() {
+ final int targetTranslationY =
+ (int) (closeOverlayBinding.closeButton.getRootView().getHeight()
+ - closeOverlayBinding.closeButton.getY());
+
+ closeOverlayBinding.closeButton.animate().setListener(null).cancel();
+ closeOverlayBinding.closeButton.animate()
+ .setInterpolator(new AnticipateInterpolator())
+ .translationY(targetTranslationY)
+ .setDuration(400)
+ .setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationCancel(final Animator animation) {
+ end();
+ }
+
+ @Override
+ public void onAnimationEnd(final Animator animation) {
+ end();
+ }
+
+ private void end() {
+ windowManager.removeView(closeOverlayBinding.getRoot());
+ closeOverlayBinding = null;
+ player.getService().stopService();
+ }
+ }).start();
+ }
+
+ @Override
+ protected float calculateMaxEndScreenThumbnailHeight(@NonNull final Bitmap bitmap) {
+ // no need for the end screen thumbnail to be resized on popup player: it's only needed
+ // for the main player so that it is enlarged correctly inside the fragment
+ return bitmap.getHeight();
+ }
+
+ private boolean popupHasParent() {
+ return binding != null
+ && binding.getRoot().getLayoutParams() instanceof WindowManager.LayoutParams
+ && binding.getRoot().getParent() != null;
+ }
+
+ private boolean anyPopupViewIsNull() {
+ return popupLayoutParams == null || windowManager == null
+ || binding.getRoot().getParent() == null;
+ }
+
+ @Override
+ public void onPlaying() {
+ super.onPlaying();
+ changePopupWindowFlags(ONGOING_PLAYBACK_WINDOW_FLAGS);
+ }
+
+ @Override
+ public void onPaused() {
+ super.onPaused();
+ changePopupWindowFlags(IDLE_WINDOW_FLAGS);
+ }
+
+ @Override
+ public void onCompleted() {
+ super.onCompleted();
+ changePopupWindowFlags(IDLE_WINDOW_FLAGS);
+ }
+
+ @Override
+ protected void setupSubtitleView(final float captionScale) {
+ final float captionRatio = (captionScale - 1.0f) / 5.0f + 1.0f;
+ binding.subtitleView.setFractionalTextSize(
+ SubtitleView.DEFAULT_TEXT_SIZE_FRACTION * captionRatio);
+ }
+
+ @Override
+ protected void onPlaybackSpeedClicked() {
+ playbackSpeedPopupMenu.show();
+ isSomePopupMenuVisible = true;
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Gestures
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Gestures
+ private int distanceFromCloseButton(@NonNull final MotionEvent popupMotionEvent) {
+ final int closeOverlayButtonX = closeOverlayBinding.closeButton.getLeft()
+ + closeOverlayBinding.closeButton.getWidth() / 2;
+ final int closeOverlayButtonY = closeOverlayBinding.closeButton.getTop()
+ + closeOverlayBinding.closeButton.getHeight() / 2;
+
+ final float fingerX = popupLayoutParams.x + popupMotionEvent.getX();
+ final float fingerY = popupLayoutParams.y + popupMotionEvent.getY();
+
+ return (int) Math.sqrt(Math.pow(closeOverlayButtonX - fingerX, 2)
+ + Math.pow(closeOverlayButtonY - fingerY, 2));
+ }
+
+ private float getClosingRadius() {
+ final int buttonRadius = closeOverlayBinding.closeButton.getWidth() / 2;
+ // 20% wider than the button itself
+ return buttonRadius * 1.2f;
+ }
+
+ public boolean isInsideClosingRadius(@NonNull final MotionEvent popupMotionEvent) {
+ return distanceFromCloseButton(popupMotionEvent) <= getClosingRadius();
+ }
+ //endregion
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Getters
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Gestures
+ public PlayerPopupCloseOverlayBinding getCloseOverlayBinding() {
+ return closeOverlayBinding;
+ }
+
+ public WindowManager.LayoutParams getPopupLayoutParams() {
+ return popupLayoutParams;
+ }
+
+ public WindowManager getWindowManager() {
+ return windowManager;
+ }
+
+ public int getScreenHeight() {
+ return screenHeight;
+ }
+
+ public int getScreenWidth() {
+ return screenWidth;
+ }
+ //endregion
+}
diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java
new file mode 100644
index 00000000000..99ecb5540a1
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java
@@ -0,0 +1,1523 @@
+package org.schabi.newpipe.player.ui;
+
+import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
+import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF;
+import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
+import static org.schabi.newpipe.MainActivity.DEBUG;
+import static org.schabi.newpipe.ktx.ViewUtils.animate;
+import static org.schabi.newpipe.ktx.ViewUtils.animateRotation;
+import static org.schabi.newpipe.player.Player.RENDERER_UNAVAILABLE;
+import static org.schabi.newpipe.player.Player.STATE_BUFFERING;
+import static org.schabi.newpipe.player.Player.STATE_COMPLETED;
+import static org.schabi.newpipe.player.Player.STATE_PAUSED;
+import static org.schabi.newpipe.player.Player.STATE_PAUSED_SEEK;
+import static org.schabi.newpipe.player.Player.STATE_PLAYING;
+import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed;
+import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString;
+import static org.schabi.newpipe.player.helper.PlayerHelper.nextResizeModeAndSaveToPrefs;
+import static org.schabi.newpipe.player.helper.PlayerHelper.retrieveSeekDurationFromPreferences;
+
+import android.content.Intent;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Handler;
+import android.util.Log;
+import android.view.GestureDetector;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.Surface;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
+import android.widget.SeekBar;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.content.res.AppCompatResources;
+import androidx.appcompat.view.ContextThemeWrapper;
+import androidx.appcompat.widget.AppCompatImageButton;
+import androidx.appcompat.widget.PopupMenu;
+import androidx.core.graphics.Insets;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.WindowInsetsCompat;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.PlaybackParameters;
+import com.google.android.exoplayer2.Player.RepeatMode;
+import com.google.android.exoplayer2.Tracks;
+import com.google.android.exoplayer2.text.Cue;
+import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
+import com.google.android.exoplayer2.ui.CaptionStyleCompat;
+import com.google.android.exoplayer2.video.VideoSize;
+
+import org.schabi.newpipe.R;
+import org.schabi.newpipe.databinding.PlayerBinding;
+import org.schabi.newpipe.extractor.MediaFormat;
+import org.schabi.newpipe.extractor.stream.StreamInfo;
+import org.schabi.newpipe.extractor.stream.VideoStream;
+import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
+import org.schabi.newpipe.ktx.AnimationType;
+import org.schabi.newpipe.player.Player;
+import org.schabi.newpipe.player.gesture.BasePlayerGestureListener;
+import org.schabi.newpipe.player.gesture.DisplayPortion;
+import org.schabi.newpipe.player.helper.PlayerHelper;
+import org.schabi.newpipe.player.mediaitem.MediaItemTag;
+import org.schabi.newpipe.player.playback.SurfaceHolderCallback;
+import org.schabi.newpipe.player.playqueue.PlayQueue;
+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.NavigationHelper;
+import org.schabi.newpipe.util.external_communication.KoreUtils;
+import org.schabi.newpipe.util.external_communication.ShareUtils;
+import org.schabi.newpipe.views.player.PlayerFastSeekOverlay;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+public abstract class VideoPlayerUi extends PlayerUi
+ implements SeekBar.OnSeekBarChangeListener, View.OnClickListener, View.OnLongClickListener, PopupMenu.OnMenuItemClickListener, PopupMenu.OnDismissListener {
+ private static final String TAG = VideoPlayerUi.class.getSimpleName();
+
+ // time constants
+ public static final long DEFAULT_CONTROLS_DURATION = 300; // 300 millis
+ public static final long DEFAULT_CONTROLS_HIDE_TIME = 2000; // 2 Seconds
+ public static final long DPAD_CONTROLS_HIDE_TIME = 7000; // 7 Seconds
+ public static final int SEEK_OVERLAY_DURATION = 450; // 450 millis
+
+ // other constants (TODO remove playback speeds and use normal menu for popup, too)
+ private static final float[] PLAYBACK_SPEEDS = {0.5f, 0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f};
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Views
+ //////////////////////////////////////////////////////////////////////////*/
+
+ protected PlayerBinding binding;
+ private final Handler controlsVisibilityHandler = new Handler();
+ @Nullable private SurfaceHolderCallback surfaceHolderCallback;
+ @Nullable private Bitmap thumbnail = null;
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Popup menus ("popup" means that they pop up, not that they belong to the popup player)
+ //////////////////////////////////////////////////////////////////////////*/
+
+ private static final int POPUP_MENU_ID_QUALITY = 69;
+ 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;
+ protected PopupMenu playbackSpeedPopupMenu;
+ private PopupMenu captionPopupMenu;
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Gestures
+ //////////////////////////////////////////////////////////////////////////*/
+
+ private GestureDetector gestureDetector;
+ private BasePlayerGestureListener playerGestureListener;
+
+ @NonNull private final SeekbarPreviewThumbnailHolder seekbarPreviewThumbnailHolder =
+ new SeekbarPreviewThumbnailHolder();
+
+ public VideoPlayerUi(@NonNull final Player player,
+ @NonNull final PlayerBinding playerBinding) {
+ super(player);
+ binding = playerBinding;
+ }
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Setup
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Setup
+ public void setupFromView() {
+ initViews();
+ initListeners();
+ setupPlayerSeekOverlay();
+ }
+
+ private void initViews() {
+ setupSubtitleView();
+
+ binding.resizeTextView
+ .setText(PlayerHelper.resizeTypeOf(context, binding.surfaceView.getResizeMode()));
+
+ binding.playbackSeekBar.getThumb()
+ .setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.SRC_IN));
+ binding.playbackSeekBar.getProgressDrawable()
+ .setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY));
+
+ final ContextThemeWrapper themeWrapper = new ContextThemeWrapper(context,
+ R.style.DarkPopupMenu);
+
+ qualityPopupMenu = new PopupMenu(themeWrapper, binding.qualityTextView);
+ playbackSpeedPopupMenu = new PopupMenu(context, binding.playbackSpeed);
+ captionPopupMenu = new PopupMenu(themeWrapper, binding.captionTextView);
+
+ binding.progressBarLoadingPanel.getIndeterminateDrawable()
+ .setColorFilter(new PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.MULTIPLY));
+
+ binding.titleTextView.setSelected(true);
+ binding.channelTextView.setSelected(true);
+
+ // Prevent hiding of bottom sheet via swipe inside queue
+ binding.itemsList.setNestedScrollingEnabled(false);
+ }
+
+ abstract BasePlayerGestureListener buildGestureListener();
+
+ protected void initListeners() {
+ binding.qualityTextView.setOnClickListener(this);
+ binding.playbackSpeed.setOnClickListener(this);
+
+ binding.playbackSeekBar.setOnSeekBarChangeListener(this);
+ binding.captionTextView.setOnClickListener(this);
+ binding.resizeTextView.setOnClickListener(this);
+ binding.playbackLiveSync.setOnClickListener(this);
+
+ playerGestureListener = buildGestureListener();
+ gestureDetector = new GestureDetector(context, playerGestureListener);
+ binding.getRoot().setOnTouchListener(playerGestureListener);
+
+ binding.repeatButton.setOnClickListener(v -> onRepeatClicked());
+ binding.shuffleButton.setOnClickListener(v -> onShuffleClicked());
+
+ binding.playPauseButton.setOnClickListener(this);
+ binding.playPreviousButton.setOnClickListener(this);
+ binding.playNextButton.setOnClickListener(this);
+
+ binding.moreOptionsButton.setOnClickListener(this);
+ binding.moreOptionsButton.setOnLongClickListener(this);
+ binding.share.setOnClickListener(this);
+ binding.share.setOnLongClickListener(this);
+ binding.fullScreenButton.setOnClickListener(this);
+ binding.screenRotationButton.setOnClickListener(this);
+ binding.playWithKodi.setOnClickListener(this);
+ binding.openInBrowser.setOnClickListener(this);
+ binding.playerCloseButton.setOnClickListener(this);
+ binding.switchMute.setOnClickListener(this);
+
+ ViewCompat.setOnApplyWindowInsetsListener(binding.itemsListPanel, (view, windowInsets) -> {
+ final Insets cutout = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout());
+ if (!cutout.equals(Insets.NONE)) {
+ view.setPadding(cutout.left, cutout.top, cutout.right, cutout.bottom);
+ }
+ return windowInsets;
+ });
+
+ // PlaybackControlRoot already consumed window insets but we should pass them to
+ // player_overlays and fast_seek_overlay too. Without it they will be off-centered.
+ binding.playbackControlRoot.addOnLayoutChangeListener(
+ (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
+ binding.playerOverlays.setPadding(
+ v.getPaddingLeft(),
+ v.getPaddingTop(),
+ v.getPaddingRight(),
+ v.getPaddingBottom());
+
+ // If we added padding to the fast seek overlay, too, it would not go under the
+ // system ui. Instead we apply negative margins equal to the window insets of
+ // the opposite side, so that the view covers all of the player (overflowing on
+ // some sides) and its center coincides with the center of other controls.
+ final RelativeLayout.LayoutParams fastSeekParams = (RelativeLayout.LayoutParams)
+ binding.fastSeekOverlay.getLayoutParams();
+ fastSeekParams.leftMargin = -v.getPaddingRight();
+ fastSeekParams.topMargin = -v.getPaddingBottom();
+ fastSeekParams.rightMargin = -v.getPaddingLeft();
+ fastSeekParams.bottomMargin = -v.getPaddingTop();
+ });
+ }
+
+ /**
+ * Initializes the Fast-For/Backward overlay.
+ */
+ private void setupPlayerSeekOverlay() {
+ binding.fastSeekOverlay
+ .seekSecondsSupplier(() -> retrieveSeekDurationFromPreferences(player) / 1000)
+ .performListener(new PlayerFastSeekOverlay.PerformListener() {
+
+ @Override
+ public void onDoubleTap() {
+ animate(binding.fastSeekOverlay, true, SEEK_OVERLAY_DURATION);
+ }
+
+ @Override
+ public void onDoubleTapEnd() {
+ animate(binding.fastSeekOverlay, false, SEEK_OVERLAY_DURATION);
+ }
+
+ @NonNull
+ @Override
+ public FastSeekDirection getFastSeekDirection(
+ @NonNull final DisplayPortion portion
+ ) {
+ if (player.exoPlayerIsNull()) {
+ // Abort seeking
+ playerGestureListener.endMultiDoubleTap();
+ return FastSeekDirection.NONE;
+ }
+ if (portion == DisplayPortion.LEFT) {
+ // Check if it's possible to rewind
+ // Small puffer to eliminate infinite rewind seeking
+ if (player.getExoPlayer().getCurrentPosition() < 500L) {
+ return FastSeekDirection.NONE;
+ }
+ return FastSeekDirection.BACKWARD;
+ } else if (portion == DisplayPortion.RIGHT) {
+ // Check if it's possible to fast-forward
+ if (player.getCurrentState() == STATE_COMPLETED
+ || player.getExoPlayer().getCurrentPosition()
+ >= player.getExoPlayer().getDuration()) {
+ return FastSeekDirection.NONE;
+ }
+ return FastSeekDirection.FORWARD;
+ }
+ /* portion == DisplayPortion.MIDDLE */
+ return FastSeekDirection.NONE;
+ }
+
+ @Override
+ public void seek(final boolean forward) {
+ playerGestureListener.keepInDoubleTapMode();
+ if (forward) {
+ player.fastForward();
+ } else {
+ player.fastRewind();
+ }
+ }
+ });
+ playerGestureListener.doubleTapControls(binding.fastSeekOverlay);
+ }
+
+ @Override
+ public void setupAfterIntent() {
+ super.setupAfterIntent();
+ setupElementsVisibility();
+ setupElementsSize(context.getResources());
+ }
+
+ @Override
+ public void initPlayer() {
+ super.initPlayer();
+ setupVideoSurface();
+ setupFromView();
+ }
+
+ @Override
+ public void initPlayback() {
+ super.initPlayback();
+
+ // #6825 - Ensure that the shuffle-button is in the correct state on the UI
+ setShuffleButton(player.getExoPlayer().getShuffleModeEnabled());
+ }
+
+ public abstract void removeViewFromParent();
+
+ @Override
+ public void destroyPlayer() {
+ super.destroyPlayer();
+ cleanupVideoSurface();
+ }
+
+ @Override
+ public void destroy() {
+ super.destroy();
+ if (binding != null) {
+ binding.endScreen.setImageBitmap(null);
+ }
+ }
+
+ protected void setupElementsVisibility() {
+ setMuteButton(player.isMuted());
+ animateRotation(binding.moreOptionsButton, DEFAULT_CONTROLS_DURATION, 0);
+ }
+
+ protected abstract void setupElementsSize(Resources resources);
+
+ protected void setupElementsSize(final int buttonsMinWidth,
+ final int playerTopPad,
+ final int controlsPad,
+ final int buttonsPad) {
+ binding.topControls.setPaddingRelative(controlsPad, playerTopPad, controlsPad, 0);
+ binding.bottomControls.setPaddingRelative(controlsPad, 0, controlsPad, 0);
+ binding.qualityTextView.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad);
+ binding.playbackSpeed.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad);
+ binding.playbackSpeed.setMinimumWidth(buttonsMinWidth);
+ binding.captionTextView.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad);
+ }
+ //endregion
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Broadcast receiver
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Broadcast receiver
+ @Override
+ public void onBroadcastReceived(final Intent intent) {
+ super.onBroadcastReceived(intent);
+ if (Intent.ACTION_CONFIGURATION_CHANGED.equals(intent.getAction())) {
+ // When the orientation changed, the screen height might be smaller.
+ // If the end screen thumbnail is not re-scaled,
+ // it can be larger than the current screen height
+ // and thus enlarging the whole player.
+ // This causes the seekbar to be ouf the visible area.
+ updateEndScreenThumbnail();
+ }
+ }
+ //endregion
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Thumbnail
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Thumbnail
+ /**
+ * Scale the player audio / end screen thumbnail down if necessary.
+ *
+ * This is necessary when the thumbnail's height is larger than the device's height
+ * and thus is enlarging the player's height
+ * causing the bottom playback controls to be out of the visible screen.
+ *
+ */
+ @Override
+ public void onThumbnailLoaded(@Nullable final Bitmap bitmap) {
+ super.onThumbnailLoaded(bitmap);
+ thumbnail = bitmap;
+ updateEndScreenThumbnail();
+ }
+
+ private void updateEndScreenThumbnail() {
+ if (thumbnail == null) {
+ // remove end screen thumbnail
+ binding.endScreen.setImageDrawable(null);
+ return;
+ }
+
+ final float endScreenHeight = calculateMaxEndScreenThumbnailHeight(thumbnail);
+ final Bitmap endScreenBitmap = Bitmap.createScaledBitmap(
+ thumbnail,
+ (int) (thumbnail.getWidth() / (thumbnail.getHeight() / endScreenHeight)),
+ (int) endScreenHeight,
+ true);
+
+ if (DEBUG) {
+ Log.d(TAG, "Thumbnail - onThumbnailLoaded() called with: "
+ + "currentThumbnail = [" + thumbnail + "], "
+ + thumbnail.getWidth() + "x" + thumbnail.getHeight()
+ + ", scaled end screen height = " + endScreenHeight
+ + ", scaled end screen width = " + endScreenBitmap.getWidth());
+ }
+
+ binding.endScreen.setImageBitmap(endScreenBitmap);
+ }
+
+ protected abstract float calculateMaxEndScreenThumbnailHeight(@NonNull final Bitmap bitmap);
+ //endregion
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Progress loop and updates
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Progress loop and updates
+ @Override
+ public void onUpdateProgress(final int currentProgress,
+ final int duration,
+ final int bufferPercent) {
+
+ if (duration != binding.playbackSeekBar.getMax()) {
+ setVideoDurationToControls(duration);
+ }
+ if (player.getCurrentState() != STATE_PAUSED) {
+ updatePlayBackElementsCurrentDuration(currentProgress);
+ }
+ if (player.isLoading() || bufferPercent > 90) {
+ binding.playbackSeekBar.setSecondaryProgress(
+ (int) (binding.playbackSeekBar.getMax() * ((float) bufferPercent / 100)));
+ }
+ if (DEBUG && bufferPercent % 20 == 0) { //Limit log
+ Log.d(TAG, "notifyProgressUpdateToListeners() called with: "
+ + "isVisible = " + isControlsVisible() + ", "
+ + "currentProgress = [" + currentProgress + "], "
+ + "duration = [" + duration + "], bufferPercent = [" + bufferPercent + "]");
+ }
+ binding.playbackLiveSync.setClickable(!player.isLiveEdge());
+ }
+
+ /**
+ * Sets the current duration into the corresponding elements.
+ */
+ private void updatePlayBackElementsCurrentDuration(final int currentProgress) {
+ // Don't set seekbar progress while user is seeking
+ if (player.getCurrentState() != STATE_PAUSED_SEEK) {
+ binding.playbackSeekBar.setProgress(currentProgress);
+ }
+ binding.playbackCurrentTime.setText(getTimeString(currentProgress));
+ }
+
+ /**
+ * Sets the video duration time into all control components (e.g. seekbar).
+ */
+ private void setVideoDurationToControls(final int duration) {
+ binding.playbackEndTime.setText(getTimeString(duration));
+
+ binding.playbackSeekBar.setMax(duration);
+ // This is important for Android TVs otherwise it would apply the default from
+ // setMax/Min methods which is (max - min) / 20
+ binding.playbackSeekBar.setKeyProgressIncrement(
+ PlayerHelper.retrieveSeekDurationFromPreferences(player));
+ }
+
+ @Override // seekbar listener
+ public void onProgressChanged(final SeekBar seekBar, final int progress,
+ final boolean fromUser) {
+ // Currently we don't need method execution when fromUser is false
+ if (!fromUser) {
+ return;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "onProgressChanged() called with: "
+ + "seekBar = [" + seekBar + "], progress = [" + progress + "]");
+ }
+
+ binding.currentDisplaySeek.setText(getTimeString(progress));
+
+ // Seekbar Preview Thumbnail
+ SeekbarPreviewThumbnailHelper
+ .tryResizeAndSetSeekbarPreviewThumbnail(
+ player.getContext(),
+ seekbarPreviewThumbnailHolder.getBitmapAt(progress),
+ binding.currentSeekbarPreviewThumbnail,
+ binding.subtitleView::getWidth);
+
+ adjustSeekbarPreviewContainer();
+ }
+
+
+ private void adjustSeekbarPreviewContainer() {
+ try {
+ // Should only be required when an error occurred before
+ // and the layout was positioned in the center
+ binding.bottomSeekbarPreviewLayout.setGravity(Gravity.NO_GRAVITY);
+
+ // Calculate the current left position of seekbar progress in px
+ // More info: https://stackoverflow.com/q/20493577
+ final int currentSeekbarLeft =
+ binding.playbackSeekBar.getLeft()
+ + binding.playbackSeekBar.getPaddingLeft()
+ + binding.playbackSeekBar.getThumb().getBounds().left;
+
+ // Calculate the (unchecked) left position of the container
+ final int uncheckedContainerLeft =
+ currentSeekbarLeft - (binding.seekbarPreviewContainer.getWidth() / 2);
+
+ // Fix the position so it's within the boundaries
+ final int checkedContainerLeft =
+ Math.max(
+ Math.min(
+ uncheckedContainerLeft,
+ // Max left
+ binding.playbackWindowRoot.getWidth()
+ - binding.seekbarPreviewContainer.getWidth()
+ ),
+ 0 // Min left
+ );
+
+ // See also: https://stackoverflow.com/a/23249734
+ final LinearLayout.LayoutParams params =
+ new LinearLayout.LayoutParams(
+ binding.seekbarPreviewContainer.getLayoutParams());
+ params.setMarginStart(checkedContainerLeft);
+ binding.seekbarPreviewContainer.setLayoutParams(params);
+ } catch (final Exception ex) {
+ Log.e(TAG, "Failed to adjust seekbarPreviewContainer", ex);
+ // Fallback - position in the middle
+ binding.bottomSeekbarPreviewLayout.setGravity(Gravity.CENTER);
+ }
+ }
+
+ @Override // seekbar listener
+ public void onStartTrackingTouch(final SeekBar seekBar) {
+ if (DEBUG) {
+ Log.d(TAG, "onStartTrackingTouch() called with: seekBar = [" + seekBar + "]");
+ }
+ if (player.getCurrentState() != STATE_PAUSED_SEEK) {
+ player.changeState(STATE_PAUSED_SEEK);
+ }
+
+ player.saveWasPlaying();
+ if (player.isPlaying()) {
+ player.getExoPlayer().pause();
+ }
+
+ showControls(0);
+ animate(binding.currentDisplaySeek, true, DEFAULT_CONTROLS_DURATION,
+ AnimationType.SCALE_AND_ALPHA);
+ animate(binding.currentSeekbarPreviewThumbnail, true, DEFAULT_CONTROLS_DURATION,
+ AnimationType.SCALE_AND_ALPHA);
+ }
+
+ @Override // seekbar listener
+ public void onStopTrackingTouch(final SeekBar seekBar) {
+ if (DEBUG) {
+ Log.d(TAG, "onStopTrackingTouch() called with: seekBar = [" + seekBar + "]");
+ }
+
+ player.seekTo(seekBar.getProgress());
+ if (player.wasPlaying() || player.getExoPlayer().getDuration() == seekBar.getProgress()) {
+ player.getExoPlayer().play();
+ }
+
+ binding.playbackCurrentTime.setText(getTimeString(seekBar.getProgress()));
+ animate(binding.currentDisplaySeek, false, 200, AnimationType.SCALE_AND_ALPHA);
+ animate(binding.currentSeekbarPreviewThumbnail, false, 200, AnimationType.SCALE_AND_ALPHA);
+
+ if (player.getCurrentState() == STATE_PAUSED_SEEK) {
+ player.changeState(STATE_BUFFERING);
+ }
+ if (!player.isProgressLoopRunning()) {
+ player.startProgressLoop();
+ }
+ if (player.wasPlaying()) {
+ showControlsThenHide();
+ }
+ }
+ //endregion
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Controls showing / hiding
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Controls showing / hiding
+
+ public boolean isControlsVisible() {
+ return binding != null && binding.playbackControlRoot.getVisibility() == View.VISIBLE;
+ }
+
+ public void showControlsThenHide() {
+ if (DEBUG) {
+ Log.d(TAG, "showControlsThenHide() called");
+ }
+
+ showOrHideButtons();
+ showSystemUIPartially();
+
+ final long hideTime = binding.playbackControlRoot.isInTouchMode()
+ ? DEFAULT_CONTROLS_HIDE_TIME
+ : DPAD_CONTROLS_HIDE_TIME;
+
+ showHideShadow(true, DEFAULT_CONTROLS_DURATION);
+ animate(binding.playbackControlRoot, true, DEFAULT_CONTROLS_DURATION,
+ AnimationType.ALPHA, 0, () -> hideControls(DEFAULT_CONTROLS_DURATION, hideTime));
+ }
+
+ public void showControls(final long duration) {
+ if (DEBUG) {
+ Log.d(TAG, "showControls() called");
+ }
+ showOrHideButtons();
+ showSystemUIPartially();
+ controlsVisibilityHandler.removeCallbacksAndMessages(null);
+ showHideShadow(true, duration);
+ animate(binding.playbackControlRoot, true, duration);
+ }
+
+ public void hideControls(final long duration, final long delay) {
+ if (DEBUG) {
+ Log.d(TAG, "hideControls() called with: duration = [" + duration
+ + "], delay = [" + delay + "]");
+ }
+
+ showOrHideButtons();
+
+ controlsVisibilityHandler.removeCallbacksAndMessages(null);
+ controlsVisibilityHandler.postDelayed(() -> {
+ showHideShadow(false, duration);
+ animate(binding.playbackControlRoot, false, duration, AnimationType.ALPHA,
+ 0, this::hideSystemUIIfNeeded);
+ }, delay);
+ }
+
+ public void showHideShadow(final boolean show, final long duration) {
+ animate(binding.playbackControlsShadow, show, duration, AnimationType.ALPHA, 0, null);
+ animate(binding.playerTopShadow, show, duration, AnimationType.ALPHA, 0, null);
+ animate(binding.playerBottomShadow, show, duration, AnimationType.ALPHA, 0, null);
+ }
+
+ protected void showOrHideButtons() {
+ @Nullable final PlayQueue playQueue = player.getPlayQueue();
+ if (playQueue == null) {
+ return;
+ }
+
+ final boolean showPrev = playQueue.getIndex() != 0;
+ final boolean showNext = playQueue.getIndex() + 1 != playQueue.getStreams().size();
+
+ binding.playPreviousButton.setVisibility(showPrev ? View.VISIBLE : View.INVISIBLE);
+ binding.playPreviousButton.setAlpha(showPrev ? 1.0f : 0.0f);
+ binding.playNextButton.setVisibility(showNext ? View.VISIBLE : View.INVISIBLE);
+ binding.playNextButton.setAlpha(showNext ? 1.0f : 0.0f);
+ }
+
+ protected void showSystemUIPartially() {
+ // system UI is really changed only by MainPlayerUi, so overridden there
+ }
+
+ protected void hideSystemUIIfNeeded() {
+ // system UI is really changed only by MainPlayerUi, so overridden there
+ }
+
+ protected boolean isAnyListViewOpen() {
+ // only MainPlayerUi has list views for the queue and for segments, so overridden there
+ return false;
+ }
+
+ public boolean isFullscreen() {
+ // only MainPlayerUi can be in fullscreen, so overridden there
+ return false;
+ }
+ //endregion
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Playback states
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Playback states
+ @Override
+ public void onPrepared() {
+ super.onPrepared();
+ setVideoDurationToControls((int) player.getExoPlayer().getDuration());
+ binding.playbackSpeed.setText(formatSpeed(player.getPlaybackSpeed()));
+ }
+
+ @Override
+ public void onBlocked() {
+ super.onBlocked();
+
+ // if we are e.g. switching players, hide controls
+ hideControls(DEFAULT_CONTROLS_DURATION, 0);
+
+ binding.playbackSeekBar.setEnabled(false);
+ binding.playbackSeekBar.getThumb()
+ .setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.SRC_IN));
+
+ binding.loadingPanel.setBackgroundColor(Color.BLACK);
+ animate(binding.loadingPanel, true, 0);
+ animate(binding.surfaceForeground, true, 100);
+
+ binding.playPauseButton.setImageResource(R.drawable.ic_play_arrow);
+ animatePlayButtons(false, 100);
+ binding.getRoot().setKeepScreenOn(false);
+ }
+
+ @Override
+ public void onPlaying() {
+ super.onPlaying();
+
+ updateStreamRelatedViews();
+
+ binding.playbackSeekBar.setEnabled(true);
+ binding.playbackSeekBar.getThumb()
+ .setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.SRC_IN));
+
+ binding.loadingPanel.setVisibility(View.GONE);
+
+ animate(binding.currentDisplaySeek, false, 200, AnimationType.SCALE_AND_ALPHA);
+
+ animate(binding.playPauseButton, false, 80, AnimationType.SCALE_AND_ALPHA, 0,
+ () -> {
+ binding.playPauseButton.setImageResource(R.drawable.ic_pause);
+ animatePlayButtons(true, 200);
+ if (!isAnyListViewOpen()) {
+ binding.playPauseButton.requestFocus();
+ }
+ });
+
+ binding.getRoot().setKeepScreenOn(true);
+ }
+
+ @Override
+ public void onBuffering() {
+ super.onBuffering();
+ binding.loadingPanel.setBackgroundColor(Color.TRANSPARENT);
+ binding.loadingPanel.setVisibility(View.VISIBLE);
+ binding.getRoot().setKeepScreenOn(true);
+ }
+
+ @Override
+ public void onPaused() {
+ super.onPaused();
+
+ // Don't let UI elements popup during double tap seeking. This state is entered sometimes
+ // during seeking/loading. This if-else check ensures that the controls aren't popping up.
+ if (!playerGestureListener.isDoubleTapping()) {
+ showControls(400);
+ binding.loadingPanel.setVisibility(View.GONE);
+
+ animate(binding.playPauseButton, false, 80, AnimationType.SCALE_AND_ALPHA, 0,
+ () -> {
+ binding.playPauseButton.setImageResource(R.drawable.ic_play_arrow);
+ animatePlayButtons(true, 200);
+ if (!isAnyListViewOpen()) {
+ binding.playPauseButton.requestFocus();
+ }
+ });
+ }
+
+ binding.getRoot().setKeepScreenOn(false);
+ }
+
+ @Override
+ public void onPausedSeek() {
+ super.onPausedSeek();
+ animatePlayButtons(false, 100);
+ binding.getRoot().setKeepScreenOn(true);
+ }
+
+ @Override
+ public void onCompleted() {
+ super.onCompleted();
+
+ animate(binding.playPauseButton, false, 0, AnimationType.SCALE_AND_ALPHA, 0,
+ () -> {
+ binding.playPauseButton.setImageResource(R.drawable.ic_replay);
+ animatePlayButtons(true, DEFAULT_CONTROLS_DURATION);
+ });
+
+ binding.getRoot().setKeepScreenOn(false);
+
+ // When a (short) video ends the elements have to display the correct values - see #6180
+ updatePlayBackElementsCurrentDuration(binding.playbackSeekBar.getMax());
+
+ showControls(500);
+ animate(binding.currentDisplaySeek, false, 200, AnimationType.SCALE_AND_ALPHA);
+ binding.loadingPanel.setVisibility(View.GONE);
+ animate(binding.surfaceForeground, true, 100);
+ }
+
+ private void animatePlayButtons(final boolean show, final long duration) {
+ animate(binding.playPauseButton, show, duration, AnimationType.SCALE_AND_ALPHA);
+
+ @Nullable final PlayQueue playQueue = player.getPlayQueue();
+ if (playQueue == null) {
+ return;
+ }
+
+ if (!show || playQueue.getIndex() > 0) {
+ animate(
+ binding.playPreviousButton,
+ show,
+ duration,
+ AnimationType.SCALE_AND_ALPHA);
+ }
+ if (!show || playQueue.getIndex() + 1 < playQueue.getStreams().size()) {
+ animate(
+ binding.playNextButton,
+ show,
+ duration,
+ AnimationType.SCALE_AND_ALPHA);
+ }
+ }
+ //endregion
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Repeat, shuffle, mute
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Repeat and shuffle
+ public void onRepeatClicked() {
+ if (DEBUG) {
+ Log.d(TAG, "onRepeatClicked() called");
+ }
+ player.cycleNextRepeatMode();
+ }
+
+ public void onShuffleClicked() {
+ if (DEBUG) {
+ Log.d(TAG, "onShuffleClicked() called");
+ }
+ player.toggleShuffleModeEnabled();
+ }
+
+ @Override
+ public void onRepeatModeChanged(@RepeatMode final int repeatMode) {
+ super.onRepeatModeChanged(repeatMode);
+ setRepeatModeButton(binding.repeatButton, repeatMode);
+ }
+
+ @Override
+ public void onShuffleModeEnabledChanged(final boolean shuffleModeEnabled) {
+ super.onShuffleModeEnabledChanged(shuffleModeEnabled);
+ setShuffleButton(shuffleModeEnabled);
+ }
+
+ @Override
+ public void onMuteUnmuteChanged(final boolean isMuted) {
+ super.onMuteUnmuteChanged(isMuted);
+ setMuteButton(isMuted);
+ }
+
+ private void setRepeatModeButton(final AppCompatImageButton imageButton,
+ @RepeatMode final int repeatMode) {
+ switch (repeatMode) {
+ case REPEAT_MODE_OFF:
+ imageButton.setImageResource(R.drawable.exo_controls_repeat_off);
+ break;
+ case REPEAT_MODE_ONE:
+ imageButton.setImageResource(R.drawable.exo_controls_repeat_one);
+ break;
+ case REPEAT_MODE_ALL:
+ imageButton.setImageResource(R.drawable.exo_controls_repeat_all);
+ break;
+ }
+ }
+
+ private void setMuteButton(final boolean isMuted) {
+ binding.switchMute.setImageDrawable(AppCompatResources.getDrawable(context, isMuted
+ ? R.drawable.ic_volume_off : R.drawable.ic_volume_up));
+ }
+
+ private void setShuffleButton(final boolean shuffled) {
+ binding.shuffleButton.setImageAlpha(shuffled ? 255 : 77);
+ }
+ //endregion
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // ExoPlayer listeners (that didn't fit in other categories)
+ //////////////////////////////////////////////////////////////////////////*/
+ //region ExoPlayer listeners (that didn't fit in other categories)
+ @Override
+ public void onTextTracksChanged(@NonNull final Tracks currentTracks) {
+ super.onTextTracksChanged(currentTracks);
+
+ final boolean trackTypeTextSupported = !currentTracks.containsType(C.TRACK_TYPE_TEXT)
+ || currentTracks.isTypeSupported(C.TRACK_TYPE_TEXT, false);
+ if (getPlayer().getTrackSelector().getCurrentMappedTrackInfo() == null
+ || !trackTypeTextSupported) {
+ binding.captionTextView.setVisibility(View.GONE);
+ return;
+ }
+
+ // Extract all loaded languages
+ final List textTracks = currentTracks
+ .getGroups()
+ .stream()
+ .filter(trackGroupInfo -> C.TRACK_TYPE_TEXT == trackGroupInfo.getType())
+ .collect(Collectors.toList());
+ final List availableLanguages = textTracks.stream()
+ .map(Tracks.Group::getMediaTrackGroup)
+ .filter(textTrack -> textTrack.length > 0)
+ .map(textTrack -> textTrack.getFormat(0).language)
+ .collect(Collectors.toList());
+
+ // Find selected text track
+ final Optional selectedTracks = textTracks.stream()
+ .filter(Tracks.Group::isSelected)
+ .filter(info -> info.getMediaTrackGroup().length >= 1)
+ .map(info -> info.getMediaTrackGroup().getFormat(0))
+ .findFirst();
+
+ // Build UI
+ buildCaptionMenu(availableLanguages);
+ //noinspection SimplifyOptionalCallChains
+ if (player.getTrackSelector().getParameters().getRendererDisabled(
+ player.getCaptionRendererIndex()) || !selectedTracks.isPresent()) {
+ binding.captionTextView.setText(R.string.caption_none);
+ } else {
+ binding.captionTextView.setText(selectedTracks.get().language);
+ }
+ binding.captionTextView.setVisibility(
+ availableLanguages.isEmpty() ? View.GONE : View.VISIBLE);
+ }
+
+ @Override
+ public void onPlaybackParametersChanged(@NonNull final PlaybackParameters playbackParameters) {
+ super.onPlaybackParametersChanged(playbackParameters);
+ binding.playbackSpeed.setText(formatSpeed(playbackParameters.speed));
+ }
+
+ @Override
+ public void onRenderedFirstFrame() {
+ super.onRenderedFirstFrame();
+ //TODO check if this causes black screen when switching to fullscreen
+ animate(binding.surfaceForeground, false, DEFAULT_CONTROLS_DURATION);
+ }
+
+ @Override
+ public void onCues(@NonNull List cues) {
+ super.onCues(cues);
+ binding.subtitleView.setCues(cues);
+ }
+ //endregion
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Metadata & stream related views
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Metadata & stream related views
+ @Override
+ public void onMetadataChanged(@NonNull final StreamInfo info) {
+ super.onMetadataChanged(info);
+
+ updateStreamRelatedViews();
+
+ binding.titleTextView.setText(info.getName());
+ binding.channelTextView.setText(info.getUploaderName());
+
+ this.seekbarPreviewThumbnailHolder.resetFrom(player.getContext(), info.getPreviewFrames());
+ }
+
+ private void updateStreamRelatedViews() {
+ //noinspection SimplifyOptionalCallChains
+ if (!player.getCurrentStreamInfo().isPresent()) {
+ return;
+ }
+ final StreamInfo info = player.getCurrentStreamInfo().get();
+
+ binding.qualityTextView.setVisibility(View.GONE);
+ binding.playbackSpeed.setVisibility(View.GONE);
+
+ binding.playbackEndTime.setVisibility(View.GONE);
+ binding.playbackLiveSync.setVisibility(View.GONE);
+
+ switch (info.getStreamType()) {
+ case AUDIO_STREAM:
+ case POST_LIVE_AUDIO_STREAM:
+ binding.surfaceView.setVisibility(View.GONE);
+ binding.endScreen.setVisibility(View.VISIBLE);
+ binding.playbackEndTime.setVisibility(View.VISIBLE);
+ break;
+
+ case AUDIO_LIVE_STREAM:
+ binding.surfaceView.setVisibility(View.GONE);
+ binding.endScreen.setVisibility(View.VISIBLE);
+ binding.playbackLiveSync.setVisibility(View.VISIBLE);
+ break;
+
+ case LIVE_STREAM:
+ binding.surfaceView.setVisibility(View.VISIBLE);
+ binding.endScreen.setVisibility(View.GONE);
+ binding.playbackLiveSync.setVisibility(View.VISIBLE);
+ break;
+
+ case VIDEO_STREAM:
+ case POST_LIVE_STREAM:
+ //noinspection SimplifyOptionalCallChains
+ if (player.getCurrentMetadata() != null
+ && !player.getCurrentMetadata().getMaybeQuality().isPresent()
+ || (info.getVideoStreams().isEmpty()
+ && info.getVideoOnlyStreams().isEmpty())) {
+ break;
+ }
+
+ buildQualityMenu();
+
+ binding.qualityTextView.setVisibility(View.VISIBLE);
+ binding.surfaceView.setVisibility(View.VISIBLE);
+ default:
+ binding.endScreen.setVisibility(View.GONE);
+ binding.playbackEndTime.setVisibility(View.VISIBLE);
+ break;
+ }
+
+ buildPlaybackSpeedMenu();
+ binding.playbackSpeed.setVisibility(View.VISIBLE);
+ }
+ //endregion
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Popup menus ("popup" means that they pop up, not that they belong to the popup player)
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Popup menus ("popup" means that they pop up, not that they belong to the popup player)
+ private void buildQualityMenu() {
+ if (qualityPopupMenu == null) {
+ return;
+ }
+ qualityPopupMenu.getMenu().removeGroup(POPUP_MENU_ID_QUALITY);
+
+ @Nullable final List availableStreams
+ = Optional.ofNullable(player.getCurrentMetadata())
+ .flatMap(MediaItemTag::getMaybeQuality)
+ .map(MediaItemTag.Quality::getSortedVideoStreams)
+ .orElse(null);
+ if (availableStreams == null) {
+ return;
+ }
+
+ for (int i = 0; i < availableStreams.size(); i++) {
+ final VideoStream videoStream = availableStreams.get(i);
+ qualityPopupMenu.getMenu().add(POPUP_MENU_ID_QUALITY, i, Menu.NONE, MediaFormat
+ .getNameById(videoStream.getFormatId()) + " " + videoStream.getResolution());
+ }
+ final VideoStream selectedVideoStream = player.getSelectedVideoStream();
+ if (selectedVideoStream != null) {
+ binding.qualityTextView.setText(selectedVideoStream.getResolution());
+ }
+ qualityPopupMenu.setOnMenuItemClickListener(this);
+ qualityPopupMenu.setOnDismissListener(this);
+ }
+
+ private void buildPlaybackSpeedMenu() {
+ if (playbackSpeedPopupMenu == null) {
+ return;
+ }
+ playbackSpeedPopupMenu.getMenu().removeGroup(POPUP_MENU_ID_PLAYBACK_SPEED);
+
+ for (int i = 0; i < PLAYBACK_SPEEDS.length; i++) {
+ playbackSpeedPopupMenu.getMenu().add(POPUP_MENU_ID_PLAYBACK_SPEED, i, Menu.NONE,
+ formatSpeed(PLAYBACK_SPEEDS[i]));
+ }
+ binding.playbackSpeed.setText(formatSpeed(player.getPlaybackSpeed()));
+ playbackSpeedPopupMenu.setOnMenuItemClickListener(this);
+ playbackSpeedPopupMenu.setOnDismissListener(this);
+ }
+
+ private void buildCaptionMenu(@NonNull final List availableLanguages) {
+ if (captionPopupMenu == null) {
+ return;
+ }
+ captionPopupMenu.getMenu().removeGroup(POPUP_MENU_ID_CAPTION);
+
+ captionPopupMenu.setOnDismissListener(this);
+
+ // Add option for turning off caption
+ final MenuItem captionOffItem = captionPopupMenu.getMenu().add(POPUP_MENU_ID_CAPTION,
+ 0, Menu.NONE, R.string.caption_none);
+ captionOffItem.setOnMenuItemClickListener(menuItem -> {
+ final int textRendererIndex = player.getCaptionRendererIndex();
+ if (textRendererIndex != RENDERER_UNAVAILABLE) {
+ player.getTrackSelector().setParameters(player.getTrackSelector()
+ .buildUponParameters().setRendererDisabled(textRendererIndex, true));
+ }
+ player.getPrefs().edit()
+ .remove(context.getString(R.string.caption_user_set_key)).apply();
+ return true;
+ });
+
+ // Add all available captions
+ for (int i = 0; i < availableLanguages.size(); i++) {
+ final String captionLanguage = availableLanguages.get(i);
+ final MenuItem captionItem = captionPopupMenu.getMenu().add(POPUP_MENU_ID_CAPTION,
+ i + 1, Menu.NONE, captionLanguage);
+ captionItem.setOnMenuItemClickListener(menuItem -> {
+ final int textRendererIndex = player.getCaptionRendererIndex();
+ if (textRendererIndex != RENDERER_UNAVAILABLE) {
+ // DefaultTrackSelector will select for text tracks in the following order.
+ // When multiple tracks share the same rank, a random track will be chosen.
+ // 1. ANY track exactly matching preferred language name
+ // 2. ANY track exactly matching preferred language stem
+ // 3. ROLE_FLAG_CAPTION track matching preferred language stem
+ // 4. ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND track matching preferred language stem
+ // This means if a caption track of preferred language is not available,
+ // then an auto-generated track of that language will be chosen automatically.
+ player.getTrackSelector().setParameters(player.getTrackSelector()
+ .buildUponParameters()
+ .setPreferredTextLanguages(captionLanguage,
+ PlayerHelper.captionLanguageStemOf(captionLanguage))
+ .setPreferredTextRoleFlags(C.ROLE_FLAG_CAPTION)
+ .setRendererDisabled(textRendererIndex, false));
+ player.getPrefs().edit().putString(context.getString(
+ R.string.caption_user_set_key), captionLanguage).apply();
+ }
+ return true;
+ });
+ }
+ captionPopupMenu.setOnDismissListener(this);
+
+ // apply caption language from previous user preference
+ final int textRendererIndex = player.getCaptionRendererIndex();
+ if (textRendererIndex == RENDERER_UNAVAILABLE) {
+ return;
+ }
+
+ // If user prefers to show no caption, then disable the renderer.
+ // Otherwise, DefaultTrackSelector may automatically find an available caption
+ // and display that.
+ final String userPreferredLanguage =
+ player.getPrefs().getString(context.getString(R.string.caption_user_set_key), null);
+ if (userPreferredLanguage == null) {
+ player.getTrackSelector().setParameters(player.getTrackSelector().buildUponParameters()
+ .setRendererDisabled(textRendererIndex, true));
+ return;
+ }
+
+ // Only set preferred language if it does not match the user preference,
+ // otherwise there might be an infinite cycle at onTextTracksChanged.
+ final List selectedPreferredLanguages =
+ player.getTrackSelector().getParameters().preferredTextLanguages;
+ if (!selectedPreferredLanguages.contains(userPreferredLanguage)) {
+ player.getTrackSelector().setParameters(player.getTrackSelector().buildUponParameters()
+ .setPreferredTextLanguages(userPreferredLanguage,
+ PlayerHelper.captionLanguageStemOf(userPreferredLanguage))
+ .setPreferredTextRoleFlags(C.ROLE_FLAG_CAPTION)
+ .setRendererDisabled(textRendererIndex, false));
+ }
+ }
+
+ protected abstract void onPlaybackSpeedClicked();
+
+ private void onQualityClicked() {
+ qualityPopupMenu.show();
+ isSomePopupMenuVisible = true;
+
+ final VideoStream videoStream = player.getSelectedVideoStream();
+ if (videoStream != null) {
+ //noinspection SetTextI18n
+ binding.qualityTextView.setText(MediaFormat.getNameById(videoStream.getFormatId())
+ + " " + videoStream.getResolution());
+ }
+
+ player.saveWasPlaying();
+ }
+
+ /**
+ * Called when an item of the quality selector or the playback speed selector is selected.
+ */
+ @Override
+ public boolean onMenuItemClick(@NonNull final MenuItem menuItem) {
+ if (DEBUG) {
+ Log.d(TAG, "onMenuItemClick() called with: "
+ + "menuItem = [" + menuItem + "], "
+ + "menuItem.getItemId = [" + menuItem.getItemId() + "]");
+ }
+
+ if (menuItem.getGroupId() == POPUP_MENU_ID_QUALITY) {
+ final int menuItemIndex = menuItem.getItemId();
+ @Nullable final MediaItemTag currentMetadata = player.getCurrentMetadata();
+ //noinspection SimplifyOptionalCallChains
+ if (currentMetadata == null || !currentMetadata.getMaybeQuality().isPresent()) {
+ return true;
+ }
+
+ final MediaItemTag.Quality quality = currentMetadata.getMaybeQuality().get();
+ final List 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());
+ return true;
+ } else if (menuItem.getGroupId() == POPUP_MENU_ID_PLAYBACK_SPEED) {
+ final int speedIndex = menuItem.getItemId();
+ final float speed = PLAYBACK_SPEEDS[speedIndex];
+
+ player.setPlaybackSpeed(speed);
+ binding.playbackSpeed.setText(formatSpeed(speed));
+ }
+
+ return false;
+ }
+
+ /**
+ * Called when some popup menu is dismissed.
+ */
+ @Override
+ public void onDismiss(@Nullable final PopupMenu menu) {
+ if (DEBUG) {
+ Log.d(TAG, "onDismiss() called with: menu = [" + menu + "]");
+ }
+ isSomePopupMenuVisible = false; //TODO check if this works
+ final VideoStream selectedVideoStream = player.getSelectedVideoStream();
+ if (selectedVideoStream != null) {
+ binding.qualityTextView.setText(selectedVideoStream.getResolution());
+ }
+ if (player.isPlaying()) {
+ hideControls(DEFAULT_CONTROLS_DURATION, 0);
+ hideSystemUIIfNeeded();
+ }
+ }
+
+ private void onCaptionClicked() {
+ if (DEBUG) {
+ Log.d(TAG, "onCaptionClicked() called");
+ }
+ captionPopupMenu.show();
+ isSomePopupMenuVisible = true;
+ }
+
+ public boolean isSomePopupMenuVisible() {
+ return isSomePopupMenuVisible;
+ }
+ //endregion
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Captions (text tracks)
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Captions (text tracks)
+ private void setupSubtitleView() {
+ setupSubtitleView(PlayerHelper.getCaptionScale(context));
+ final CaptionStyleCompat captionStyle = PlayerHelper.getCaptionStyle(context);
+ binding.subtitleView.setApplyEmbeddedStyles(captionStyle == CaptionStyleCompat.DEFAULT);
+ binding.subtitleView.setStyle(captionStyle);
+ }
+
+ protected abstract void setupSubtitleView(final float captionScale);
+ //endregion
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Click listeners
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Click listeners
+ @Override
+ public void onClick(final View v) {
+ if (DEBUG) {
+ Log.d(TAG, "onClick() called with: v = [" + v + "]");
+ }
+ if (v.getId() == binding.resizeTextView.getId()) {
+ onResizeClicked();
+ } else if (v.getId() == binding.captionTextView.getId()) {
+ onCaptionClicked();
+ } else if (v.getId() == binding.playbackLiveSync.getId()) {
+ player.seekToDefault();
+ } else if (v.getId() == binding.playPauseButton.getId()) {
+ player.playPause();
+ } else if (v.getId() == binding.playPreviousButton.getId()) {
+ player.playPrevious();
+ } else if (v.getId() == binding.playNextButton.getId()) {
+ player.playNext();
+ } else if (v.getId() == binding.moreOptionsButton.getId()) {
+ onMoreOptionsClicked();
+ } else if (v.getId() == binding.share.getId()) {
+ final PlayQueueItem currentItem = player.getCurrentItem();
+ if (currentItem != null) {
+ ShareUtils.shareText(context, currentItem.getTitle(),
+ player.getVideoUrlAtCurrentTime(), currentItem.getThumbnailUrl());
+ }
+ } else if (v.getId() == binding.playWithKodi.getId()) {
+ onPlayWithKodiClicked();
+ } else if (v.getId() == binding.openInBrowser.getId()) {
+ onOpenInBrowserClicked();
+ } else if (v.getId() == binding.fullScreenButton.getId()) {
+ player.setRecovery();
+ NavigationHelper.playOnMainPlayer(context, player.getPlayQueue(), true);
+ return;
+ } else if (v.getId() == binding.switchMute.getId()) {
+ player.toggleMute();
+ } else if (v.getId() == binding.playerCloseButton.getId()) {
+ context.sendBroadcast(new Intent(VideoDetailFragment.ACTION_HIDE_MAIN_PLAYER));
+ } else if (v.getId() == binding.playbackSpeed.getId()) {
+ onPlaybackSpeedClicked();
+ } else if (v.getId() == binding.qualityTextView.getId()) {
+ onQualityClicked();
+ }
+
+ manageControlsAfterOnClick(v);
+ }
+
+ /**
+ * Manages the controls after a click occurred on the player UI.
+ * @param v – The view that was clicked
+ */
+ public void manageControlsAfterOnClick(@NonNull final View v) {
+ if (player.getCurrentState() == STATE_COMPLETED) {
+ return;
+ }
+
+ controlsVisibilityHandler.removeCallbacksAndMessages(null);
+ showHideShadow(true, DEFAULT_CONTROLS_DURATION);
+ animate(binding.playbackControlRoot, true, DEFAULT_CONTROLS_DURATION,
+ AnimationType.ALPHA, 0, () -> {
+ if (player.getCurrentState() == STATE_PLAYING && !isSomePopupMenuVisible) {
+ if (v.getId() == binding.playPauseButton.getId()
+ // Hide controls in fullscreen immediately
+ || (v.getId() == binding.screenRotationButton.getId()
+ && isFullscreen())) {
+ hideControls(0, 0);
+ } else {
+ hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
+ }
+ }
+ });
+ }
+
+ @Override
+ public boolean onLongClick(final View v) {
+ if (v.getId() == binding.share.getId()) {
+ ShareUtils.copyToClipboard(context, player.getVideoUrlAtCurrentTime());
+ }
+ return true;
+ }
+
+ public boolean onKeyDown(final int keyCode) {
+ switch (keyCode) {
+ default:
+ break;
+ case KeyEvent.KEYCODE_BACK:
+ if (DeviceUtils.isTv(context) && isControlsVisible()) {
+ hideControls(0, 0);
+ return true;
+ }
+ break;
+ case KeyEvent.KEYCODE_DPAD_UP:
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ if ((binding.getRoot().hasFocus() && !binding.playbackControlRoot.hasFocus())
+ || isAnyListViewOpen()) {
+ // do not interfere with focus in playlist and play queue etc.
+ return false;
+ }
+
+ if (player.getCurrentState() == org.schabi.newpipe.player.Player.STATE_BLOCKED) {
+ return true;
+ }
+
+ if (isControlsVisible()) {
+ hideControls(DEFAULT_CONTROLS_DURATION, DPAD_CONTROLS_HIDE_TIME);
+ } else {
+ binding.playPauseButton.requestFocus();
+ showControlsThenHide();
+ showSystemUIPartially();
+ return true;
+ }
+ break;
+ }
+
+ return false;
+ }
+
+ private void onMoreOptionsClicked() {
+ if (DEBUG) {
+ Log.d(TAG, "onMoreOptionsClicked() called");
+ }
+
+ final boolean isMoreControlsVisible =
+ binding.secondaryControls.getVisibility() == View.VISIBLE;
+
+ animateRotation(binding.moreOptionsButton, DEFAULT_CONTROLS_DURATION,
+ isMoreControlsVisible ? 0 : 180);
+ animate(binding.secondaryControls, !isMoreControlsVisible, DEFAULT_CONTROLS_DURATION,
+ AnimationType.SLIDE_AND_ALPHA, 0, () -> {
+ // Fix for a ripple effect on background drawable.
+ // When view returns from GONE state it takes more milliseconds than returning
+ // from INVISIBLE state. And the delay makes ripple background end to fast
+ if (isMoreControlsVisible) {
+ binding.secondaryControls.setVisibility(View.INVISIBLE);
+ }
+ });
+ showControls(DEFAULT_CONTROLS_DURATION);
+ }
+
+ private void onPlayWithKodiClicked() {
+ if (player.getCurrentMetadata() != null) {
+ player.pause();
+ try {
+ NavigationHelper.playWithKore(context, Uri.parse(player.getVideoUrl()));
+ } catch (final Exception e) {
+ if (DEBUG) {
+ Log.i(TAG, "Failed to start kore", e);
+ }
+ KoreUtils.showInstallKoreDialog(player.getContext());
+ }
+ }
+ }
+
+ private void onOpenInBrowserClicked() {
+ player.getCurrentStreamInfo().ifPresent(streamInfo ->
+ ShareUtils.openUrlInBrowser(player.getContext(), streamInfo.getOriginalUrl()));
+ }
+ //endregion
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Video size, resize, orientation, fullscreen
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Video size, resize, orientation, fullscreen
+ protected void setResizeMode(@AspectRatioFrameLayout.ResizeMode final int resizeMode) {
+ binding.surfaceView.setResizeMode(resizeMode);
+ binding.resizeTextView.setText(PlayerHelper.resizeTypeOf(context, resizeMode));
+ }
+
+ void onResizeClicked() {
+ setResizeMode(nextResizeModeAndSaveToPrefs(player, binding.surfaceView.getResizeMode()));
+ }
+
+ @Override
+ public void onVideoSizeChanged(@NonNull VideoSize videoSize) {
+ super.onVideoSizeChanged(videoSize);
+ binding.surfaceView.setAspectRatio(((float) videoSize.width) / videoSize.height);
+ }
+ //endregion
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // SurfaceHolderCallback helpers
+ //////////////////////////////////////////////////////////////////////////*/
+ //region SurfaceHolderCallback helpers
+ private void setupVideoSurface() {
+ // make sure there is nothing left over from previous calls
+ cleanupVideoSurface();
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // >=API23
+ surfaceHolderCallback = new SurfaceHolderCallback(context, player.getExoPlayer());
+ binding.surfaceView.getHolder().addCallback(surfaceHolderCallback);
+ final Surface surface = binding.surfaceView.getHolder().getSurface();
+
+ // ensure player is using an unreleased surface, which the surfaceView might not be
+ // when starting playback on background or during player switching
+ if (surface.isValid()) {
+ // initially set the surface manually otherwise
+ // onRenderedFirstFrame() will not be called
+ player.getExoPlayer().setVideoSurface(surface);
+ }
+
+ } else {
+ player.getExoPlayer().setVideoSurfaceView(binding.surfaceView);
+ }
+ }
+
+ private void cleanupVideoSurface() {
+ final Optional exoPlayer = Optional.ofNullable(player.getExoPlayer());
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // >=API23
+ if (surfaceHolderCallback != null) {
+ binding.surfaceView.getHolder().removeCallback(surfaceHolderCallback);
+ surfaceHolderCallback.release();
+ surfaceHolderCallback = null;
+ }
+ exoPlayer.ifPresent(simpleExoPlayer -> simpleExoPlayer.setVideoSurface(null));
+ } else {
+ exoPlayer.ifPresent(simpleExoPlayer -> simpleExoPlayer.setVideoSurfaceView(null));
+ }
+ }
+ //endregion
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Getters
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Getters
+ public PlayerBinding getBinding() {
+ return binding;
+ }
+
+ public GestureDetector getGestureDetector() {
+ return gestureDetector;
+ }
+ //endregion
+}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/custom/NotificationActionsPreference.java b/app/src/main/java/org/schabi/newpipe/settings/custom/NotificationActionsPreference.java
index 849574171c1..0eb58f7a9a4 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/custom/NotificationActionsPreference.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/custom/NotificationActionsPreference.java
@@ -26,7 +26,7 @@
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
-import org.schabi.newpipe.player.MainPlayer;
+import org.schabi.newpipe.player.PlayerService;
import org.schabi.newpipe.player.NotificationConstants;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ThemeHelper;
@@ -61,7 +61,7 @@ public void onBindViewHolder(@NonNull final PreferenceViewHolder holder) {
public void onDetached() {
super.onDetached();
saveChanges();
- getContext().sendBroadcast(new Intent(MainPlayer.ACTION_RECREATE_NOTIFICATION));
+ getContext().sendBroadcast(new Intent(PlayerService.ACTION_RECREATE_NOTIFICATION));
}
diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java
index c40b1a43081..36b2bd46d4f 100644
--- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java
@@ -50,8 +50,8 @@
import org.schabi.newpipe.local.playlist.LocalPlaylistFragment;
import org.schabi.newpipe.local.subscription.SubscriptionFragment;
import org.schabi.newpipe.local.subscription.SubscriptionsImportFragment;
-import org.schabi.newpipe.player.MainPlayer;
-import org.schabi.newpipe.player.MainPlayer.PlayerType;
+import org.schabi.newpipe.player.PlayerService;
+import org.schabi.newpipe.player.PlayerService.PlayerType;
import org.schabi.newpipe.player.PlayQueueActivity;
import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.helper.PlayerHelper;
@@ -91,7 +91,7 @@ public static Intent getPlayerIntent(@NonNull final Context context,
intent.putExtra(Player.PLAY_QUEUE_KEY, cacheKey);
}
}
- intent.putExtra(Player.PLAYER_TYPE, MainPlayer.PlayerType.VIDEO.ordinal());
+ intent.putExtra(Player.PLAYER_TYPE, PlayerService.PlayerType.MAIN.ordinal());
intent.putExtra(Player.RESUME_PLAYBACK, resumePlayback);
return intent;
@@ -163,8 +163,8 @@ public static void playOnPopupPlayer(final Context context,
Toast.makeText(context, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show();
- final Intent intent = getPlayerIntent(context, MainPlayer.class, queue, resumePlayback);
- intent.putExtra(Player.PLAYER_TYPE, MainPlayer.PlayerType.POPUP.ordinal());
+ final Intent intent = getPlayerIntent(context, PlayerService.class, queue, resumePlayback);
+ intent.putExtra(Player.PLAYER_TYPE, PlayerService.PlayerType.POPUP.ordinal());
ContextCompat.startForegroundService(context, intent);
}
@@ -174,8 +174,8 @@ public static void playOnBackgroundPlayer(final Context context,
Toast.makeText(context, R.string.background_player_playing_toast, Toast.LENGTH_SHORT)
.show();
- final Intent intent = getPlayerIntent(context, MainPlayer.class, queue, resumePlayback);
- intent.putExtra(Player.PLAYER_TYPE, MainPlayer.PlayerType.AUDIO.ordinal());
+ final Intent intent = getPlayerIntent(context, PlayerService.class, queue, resumePlayback);
+ intent.putExtra(Player.PLAYER_TYPE, PlayerService.PlayerType.AUDIO.ordinal());
ContextCompat.startForegroundService(context, intent);
}
@@ -184,7 +184,7 @@ public static void enqueueOnPlayer(final Context context,
final PlayQueue queue,
final PlayerType playerType) {
Toast.makeText(context, R.string.enqueued, Toast.LENGTH_SHORT).show();
- final Intent intent = getPlayerEnqueueIntent(context, MainPlayer.class, queue);
+ final Intent intent = getPlayerEnqueueIntent(context, PlayerService.class, queue);
intent.putExtra(Player.PLAYER_TYPE, playerType.ordinal());
ContextCompat.startForegroundService(context, intent);
@@ -194,7 +194,7 @@ public static void enqueueOnPlayer(final Context context, final PlayQueue queue)
PlayerType playerType = PlayerHolder.getInstance().getType();
if (!PlayerHolder.getInstance().isPlayerOpen()) {
Log.e(TAG, "Enqueueing but no player is open; defaulting to background player");
- playerType = MainPlayer.PlayerType.AUDIO;
+ playerType = PlayerService.PlayerType.AUDIO;
}
enqueueOnPlayer(context, queue, playerType);
@@ -205,10 +205,10 @@ public static void enqueueNextOnPlayer(final Context context, final PlayQueue qu
PlayerType playerType = PlayerHolder.getInstance().getType();
if (!PlayerHolder.getInstance().isPlayerOpen()) {
Log.e(TAG, "Enqueueing next but no player is open; defaulting to background player");
- playerType = MainPlayer.PlayerType.AUDIO;
+ playerType = PlayerService.PlayerType.AUDIO;
}
Toast.makeText(context, R.string.enqueued_next, Toast.LENGTH_SHORT).show();
- final Intent intent = getPlayerEnqueueNextIntent(context, MainPlayer.class, queue);
+ final Intent intent = getPlayerEnqueueNextIntent(context, PlayerService.class, queue);
intent.putExtra(Player.PLAYER_TYPE, playerType.ordinal());
ContextCompat.startForegroundService(context, intent);
@@ -414,14 +414,14 @@ public static void openVideoDetailFragment(@NonNull final Context context,
final boolean switchingPlayers) {
final boolean autoPlay;
- @Nullable final MainPlayer.PlayerType playerType = PlayerHolder.getInstance().getType();
+ @Nullable final PlayerService.PlayerType playerType = PlayerHolder.getInstance().getType();
if (!PlayerHolder.getInstance().isPlayerOpen()) {
// no player open
autoPlay = PlayerHelper.isAutoplayAllowedByUser(context);
} else if (switchingPlayers) {
// switching player to main player
autoPlay = PlayerHolder.getInstance().isPlaying(); // keep play/pause state
- } else if (playerType == MainPlayer.PlayerType.VIDEO) {
+ } else if (playerType == PlayerService.PlayerType.MAIN) {
// opening new stream while already playing in main player
autoPlay = PlayerHelper.isAutoplayAllowedByUser(context);
} else {
@@ -436,7 +436,7 @@ public static void openVideoDetailFragment(@NonNull final Context context,
// Situation when user switches from players to main player. All needed data is
// here, we can start watching (assuming newQueue equals playQueue).
// Starting directly in fullscreen if the previous player type was popup.
- detailFragment.openVideoPlayer(playerType == MainPlayer.PlayerType.POPUP
+ detailFragment.openVideoPlayer(playerType == PlayerService.PlayerType.POPUP
|| PlayerHelper.isStartMainPlayerFullscreenEnabled(context));
} else {
detailFragment.selectAndLoadVideo(serviceId, url, title, playQueue);
diff --git a/app/src/main/java/org/schabi/newpipe/views/player/PlayerFastSeekOverlay.kt b/app/src/main/java/org/schabi/newpipe/views/player/PlayerFastSeekOverlay.kt
index 649b604943a..cbba0a75b54 100644
--- a/app/src/main/java/org/schabi/newpipe/views/player/PlayerFastSeekOverlay.kt
+++ b/app/src/main/java/org/schabi/newpipe/views/player/PlayerFastSeekOverlay.kt
@@ -12,8 +12,8 @@ import androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.START
import androidx.constraintlayout.widget.ConstraintSet
import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.R
-import org.schabi.newpipe.player.event.DisplayPortion
-import org.schabi.newpipe.player.event.DoubleTapListener
+import org.schabi.newpipe.player.gesture.DisplayPortion
+import org.schabi.newpipe.player.gesture.DoubleTapListener
class PlayerFastSeekOverlay(context: Context, attrs: AttributeSet?) :
ConstraintLayout(context, attrs), DoubleTapListener {
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 97ccd199ecb..01d84281237 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -25,7 +25,7 @@
android:layout_gravity="center_horizontal"
app:behavior_hideable="true"
app:behavior_peekHeight="0dp"
- app:layout_behavior="org.schabi.newpipe.player.event.CustomBottomSheetBehavior" />
+ app:layout_behavior="org.schabi.newpipe.player.gesture.CustomBottomSheetBehavior" />
From b3f99645a39005ddfad19f61b98b272c81414470 Mon Sep 17 00:00:00 2001
From: Stypox
Date: Sat, 9 Apr 2022 10:48:34 +0200
Subject: [PATCH 122/240] Fix some crashes / issues after player refactor
---
.../fragments/detail/VideoDetailFragment.java | 28 ++--
.../org/schabi/newpipe/player/Player.java | 8 +-
.../gesture/MainPlayerGestureListener.kt | 15 +--
.../newpipe/player/ui/MainPlayerUi.java | 95 ++++++++-----
.../schabi/newpipe/player/ui/PlayerUi.java | 1 -
.../newpipe/player/ui/PopupPlayerUi.java | 35 +++--
.../newpipe/player/ui/VideoPlayerUi.java | 127 +++++++++++++-----
.../views/player/PlayerFastSeekOverlay.kt | 6 +-
8 files changed, 194 insertions(+), 121 deletions(-)
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java
index 5ecc35034dc..cb8f0961f5d 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java
@@ -240,10 +240,6 @@ public void onServiceConnected(final Player connectedPlayer,
playerUi.ifPresent(MainPlayerUi::toggleFullscreen);
}
- if (playerIsNotStopped() && player.videoPlayerSelected()) {
- addVideoPlayerView();
- }
-
//noinspection SimplifyOptionalCallChains
if (playAfterConnect
|| (currentInfo != null
@@ -335,6 +331,9 @@ public void onPause() {
@Override
public void onResume() {
super.onResume();
+ if (DEBUG) {
+ Log.d(TAG, "onResume() called");
+ }
activity.sendBroadcast(new Intent(ACTION_VIDEO_FRAGMENT_RESUMED));
@@ -1310,22 +1309,14 @@ private void addVideoPlayerView() {
if (!isPlayerAvailable()) {
return;
}
-
- final Optional root = player.UIs().get(VideoPlayerUi.class)
- .map(VideoPlayerUi::getBinding)
- .map(ViewBinding::getRoot);
-
- // Check if viewHolder already contains a child TODO TODO whaat
- /*if (playerService != null
- && root.map(View::getParent).orElse(null) != binding.playerPlaceholder) {
- playerService.removeViewFromParent();
- }*/
setHeightThumbnail();
// Prevent from re-adding a view multiple times
- if (root.isPresent() && root.get().getParent() == null) {
- binding.playerPlaceholder.addView(root.get());
- }
+ new Handler().post(() -> player.UIs().get(MainPlayerUi.class).ifPresent(playerUi -> {
+ playerUi.removeViewFromParent();
+ binding.playerPlaceholder.addView(playerUi.getBinding().getRoot());
+ playerUi.setupVideoSurfaceIfNeeded();
+ }));
}
private void removeVideoPlayerView() {
@@ -1793,9 +1784,6 @@ private void showPlaybackProgress(final long progress, final long duration) {
@Override
public void onViewCreated() {
- // Video view can have elements visible from popup,
- // We hide it here but once it ready the view will be shown in handleIntent()
- getRoot().ifPresent(view -> view.setVisibility(View.GONE));
addVideoPlayerView();
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java
index 284ab74d8c3..78e93970c70 100644
--- a/app/src/main/java/org/schabi/newpipe/player/Player.java
+++ b/app/src/main/java/org/schabi/newpipe/player/Player.java
@@ -485,6 +485,10 @@ private void initUIsForCurrentPlayerType() {
// make sure UIs know whether a service is connected or not
UIs.call(PlayerUi::onFragmentListenerSet);
}
+ if (!exoPlayerIsNull()) {
+ UIs.call(PlayerUi::initPlayer);
+ UIs.call(PlayerUi::initPlayback);
+ }
}
private void initPlayback(@NonNull final PlayQueue queue,
@@ -599,7 +603,7 @@ public void destroy() {
progressUpdateDisposable.set(null);
PicassoHelper.cancelTag(PicassoHelper.PLAYER_THUMBNAIL_TAG); // cancel thumbnail loading
- UIs.call(PlayerUi::destroy);
+ UIs.destroyAll(Object.class); // destroy every UI: obviously every UI extends Object
}
public void setRecovery() {
@@ -737,7 +741,7 @@ private void onBroadcastReceived(final Intent intent) {
case Intent.ACTION_CONFIGURATION_CHANGED:
assureCorrectAppLanguage(service);
if (DEBUG) {
- Log.d(TAG, "onConfigurationChanged() called");
+ Log.d(TAG, "ACTION_CONFIGURATION_CHANGED received");
}
break;
case Intent.ACTION_HEADSET_PLUG: //FIXME
diff --git a/app/src/main/java/org/schabi/newpipe/player/gesture/MainPlayerGestureListener.kt b/app/src/main/java/org/schabi/newpipe/player/gesture/MainPlayerGestureListener.kt
index 17205fb9ae5..81e216006c9 100644
--- a/app/src/main/java/org/schabi/newpipe/player/gesture/MainPlayerGestureListener.kt
+++ b/app/src/main/java/org/schabi/newpipe/player/gesture/MainPlayerGestureListener.kt
@@ -1,12 +1,12 @@
package org.schabi.newpipe.player.gesture
-import android.app.Activity
import android.content.Context
import android.util.Log
import android.view.MotionEvent
import android.view.View
import android.view.View.OnTouchListener
import android.widget.ProgressBar
+import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.R
@@ -29,8 +29,6 @@ import kotlin.math.min
class MainPlayerGestureListener(
private val playerUi: MainPlayerUi
) : BasePlayerGestureListener(playerUi), OnTouchListener {
- private val maxVolume: Int = player.audioReactor.maxVolume
-
private var isMoving = false
override fun onTouch(v: View, event: MotionEvent): Boolean {
@@ -41,11 +39,11 @@ class MainPlayerGestureListener(
}
return when (event.action) {
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
- v.parent.requestDisallowInterceptTouchEvent(playerUi.isFullscreen)
+ v.parent?.requestDisallowInterceptTouchEvent(playerUi.isFullscreen)
true
}
MotionEvent.ACTION_UP -> {
- v.parent.requestDisallowInterceptTouchEvent(false)
+ v.parent?.requestDisallowInterceptTouchEvent(false)
false
}
else -> true
@@ -68,14 +66,15 @@ class MainPlayerGestureListener(
private fun onScrollVolume(distanceY: Float) {
// If we just started sliding, change the progress bar to match the system volume
if (binding.volumeRelativeLayout.visibility != View.VISIBLE) {
- val volumePercent: Float = player.audioReactor.volume / maxVolume.toFloat()
+ val volumePercent: Float =
+ player.audioReactor.volume / player.audioReactor.maxVolume.toFloat()
binding.volumeProgressBar.progress = (volumePercent * MAX_GESTURE_LENGTH).toInt()
}
binding.volumeProgressBar.incrementProgressBy(distanceY.toInt())
val currentProgressPercent: Float =
binding.volumeProgressBar.progress.toFloat() / MAX_GESTURE_LENGTH
- val currentVolume = (maxVolume * currentProgressPercent).toInt()
+ val currentVolume = (player.audioReactor.maxVolume * currentProgressPercent).toInt()
player.audioReactor.volume = currentVolume
if (DEBUG) {
Log.d(TAG, "onScroll().volumeControl, currentVolume = $currentVolume")
@@ -102,7 +101,7 @@ class MainPlayerGestureListener(
}
private fun onScrollBrightness(distanceY: Float) {
- val parent: Activity = playerUi.parentActivity
+ val parent: AppCompatActivity = playerUi.parentActivity.orElse(null) ?: return
val window = parent.window
val layoutParams = window.attributes
val bar: ProgressBar = binding.brightnessProgressBar
diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java
index 10ed424bab8..7c60671dd75 100644
--- a/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java
+++ b/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java
@@ -13,12 +13,13 @@
import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString;
import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientationLocked;
+import android.app.Activity;
+import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.database.ContentObserver;
import android.graphics.Bitmap;
import android.graphics.Color;
-import android.os.Build;
import android.os.Handler;
import android.provider.Settings;
import android.util.DisplayMetrics;
@@ -28,7 +29,6 @@
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
-import android.view.Window;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
@@ -37,6 +37,7 @@
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.content.res.AppCompatResources;
+import androidx.fragment.app.FragmentActivity;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
@@ -68,8 +69,9 @@
import java.util.List;
import java.util.Objects;
+import java.util.Optional;
-public final class MainPlayerUi extends VideoPlayerUi {
+public final class MainPlayerUi extends VideoPlayerUi implements View.OnLayoutChangeListener {
private static final String TAG = MainPlayerUi.class.getSimpleName();
private boolean isFullscreen = false;
@@ -113,7 +115,6 @@ public void setupAfterIntent() {
super.setupAfterIntent();
- binding.getRoot().setVisibility(View.VISIBLE);
initVideoPlayer();
// Android TV: without it focus will frame the whole player
binding.playPauseButton.requestFocus();
@@ -139,7 +140,8 @@ protected void initListeners() {
binding.segmentsButton.setOnClickListener(v -> onSegmentsClicked());
binding.addToPlaylistButton.setOnClickListener(v ->
- player.onAddToPlaylistClicked(getParentActivity().getSupportFragmentManager()));
+ getParentActivity().map(FragmentActivity::getSupportFragmentManager)
+ .ifPresent(player::onAddToPlaylistClicked));
settingsContentObserver = new ContentObserver(new Handler()) {
@Override
@@ -151,7 +153,20 @@ public void onChange(final boolean selfChange) {
Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION), false,
settingsContentObserver);
- binding.getRoot().addOnLayoutChangeListener(this::onLayoutChange);
+ binding.getRoot().addOnLayoutChangeListener(this);
+ }
+
+ @Override
+ protected void deinitListeners() {
+ super.deinitListeners();
+
+ binding.queueButton.setOnClickListener(null);
+ binding.segmentsButton.setOnClickListener(null);
+ binding.addToPlaylistButton.setOnClickListener(null);
+
+ context.getContentResolver().unregisterContentObserver(settingsContentObserver);
+
+ binding.getRoot().removeOnLayoutChangeListener(this);
}
@Override
@@ -178,7 +193,6 @@ public void removeViewFromParent() {
@Override
public void destroy() {
super.destroy();
- context.getContentResolver().unregisterContentObserver(settingsContentObserver);
// Exit from fullscreen when user closes the player via notification
if (isFullscreen) {
@@ -324,9 +338,10 @@ private void onFragmentStopped() {
player.useVideoSource(false);
break;
case MINIMIZE_ON_EXIT_MODE_POPUP:
- player.setRecovery();
- NavigationHelper.playOnPopupPlayer(getParentActivity(),
- player.getPlayQueue(), true);
+ getParentActivity().ifPresent(activity -> {
+ player.setRecovery();
+ NavigationHelper.playOnPopupPlayer(activity, player.getPlayQueue(), true);
+ });
break;
case MINIMIZE_ON_EXIT_MODE_NONE: default:
player.pause();
@@ -385,14 +400,15 @@ protected void showOrHideButtons() {
@Override
public void showSystemUIPartially() {
if (isFullscreen) {
- final Window window = getParentActivity().getWindow();
- window.setStatusBarColor(Color.TRANSPARENT);
- window.setNavigationBarColor(Color.TRANSPARENT);
- final int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
- | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
- | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
- window.getDecorView().setSystemUiVisibility(visibility);
- window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
+ getParentActivity().map(Activity::getWindow).ifPresent(window -> {
+ window.setStatusBarColor(Color.TRANSPARENT);
+ window.setNavigationBarColor(Color.TRANSPARENT);
+ final int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+ | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
+ window.getDecorView().setSystemUiVisibility(visibility);
+ window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
+ });
}
}
@@ -476,8 +492,9 @@ protected void setupSubtitleView(float captionScale) {
//region Gestures
@SuppressWarnings("checkstyle:ParameterNumber")
- private void onLayoutChange(final View view, final int l, final int t, final int r, final int b,
- final int ol, final int ot, final int or, final int ob) {
+ @Override
+ public void onLayoutChange(final View view, final int l, final int t, final int r, final int b,
+ final int ol, final int ot, final int or, final int ob) {
if (l != ol || t != ot || r != or || b != ob) {
// Use smaller value to be consistent between screen orientations
// (and to make usage easier)
@@ -501,9 +518,8 @@ private void onLayoutChange(final View view, final int l, final int t, final int
private void setInitialGestureValues() {
if (player.getAudioReactor() != null) {
- final float currentVolumeNormalized =
- (float) player.getAudioReactor().getVolume()
- / player.getAudioReactor().getMaxVolume();
+ final float currentVolumeNormalized = (float) player.getAudioReactor().getVolume()
+ / player.getAudioReactor().getMaxVolume();
binding.volumeProgressBar.setProgress(
(int) (binding.volumeProgressBar.getMax() * currentVolumeNormalized));
}
@@ -714,7 +730,7 @@ public void selected(final PlayQueueItem item, final View view) {
@Override
public void held(final PlayQueueItem item, final View view) {
@Nullable final PlayQueue playQueue = player.getPlayQueue();
- @Nullable final AppCompatActivity parentActivity = getParentActivity();
+ @Nullable final AppCompatActivity parentActivity = getParentActivity().orElse(null);
if (playQueue != null && parentActivity != null && playQueue.indexOf(item) != -1) {
openPopupMenu(player.getPlayQueue(), item, view, true,
parentActivity.getSupportFragmentManager(), context);
@@ -801,10 +817,15 @@ public void onClick(final View v) {
@Override
protected void onPlaybackSpeedClicked() {
+ final AppCompatActivity activity = getParentActivity().orElse(null);
+ if (activity == null) {
+ return;
+ }
+
PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), player.getPlaybackPitch(),
player.getPlaybackSkipSilence(), (speed, pitch, skipSilence)
-> player.setPlaybackParameters(speed, pitch, skipSilence))
- .show(getParentActivity().getSupportFragmentManager(), null);
+ .show(activity.getSupportFragmentManager(), null);
}
@Override
@@ -876,15 +897,15 @@ public void toggleFullscreen() {
}
isFullscreen = !isFullscreen;
- if (!isFullscreen) {
- // Apply window insets because Android will not do it when orientation changes
- // from landscape to portrait (open vertical video to reproduce)
- binding.playbackControlRoot.setPadding(0, 0, 0, 0);
- } else {
+ if (isFullscreen) {
// Android needs tens milliseconds to send new insets but a user is able to see
// how controls changes it's position from `0` to `nav bar height` padding.
// So just hide the controls to hide this visual inconsistency
hideControls(0, 0);
+ } else {
+ // Apply window insets because Android will not do it when orientation changes
+ // from landscape to portrait (open vertical video to reproduce)
+ binding.playbackControlRoot.setPadding(0, 0, 0, 0);
}
fragmentListener.onFullscreenStateChanged(isFullscreen);
@@ -924,14 +945,22 @@ public PlayerBinding getBinding() {
return binding;
}
- public AppCompatActivity getParentActivity() {
- return (AppCompatActivity) ((ViewGroup) binding.getRoot().getParent()).getContext();
+ public Optional getParentActivity() {
+ final ViewParent rootParent = binding.getRoot().getParent();
+ if (rootParent instanceof ViewGroup) {
+ final Context activity = ((ViewGroup) rootParent).getContext();
+ if (activity instanceof AppCompatActivity) {
+ return Optional.of((AppCompatActivity) activity);
+ }
+ }
+ return Optional.empty();
}
public boolean isLandscape() {
// DisplayMetrics from activity context knows about MultiWindow feature
// while DisplayMetrics from app context doesn't
- return DeviceUtils.isLandscape(getParentActivity());
+ return DeviceUtils.isLandscape(
+ getParentActivity().map(Context.class::cast).orElse(player.getService()));
}
//endregion
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java
index fd63790d6f7..15b468fb715 100644
--- a/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java
+++ b/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java
@@ -19,7 +19,6 @@
import java.util.List;
public abstract class PlayerUi {
- private static final String TAG = PlayerUi.class.getSimpleName();
@NonNull protected Context context;
@NonNull protected Player player;
diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/PopupPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/PopupPlayerUi.java
index b8a26a23314..7df9102b75d 100644
--- a/app/src/main/java/org/schabi/newpipe/player/ui/PopupPlayerUi.java
+++ b/app/src/main/java/org/schabi/newpipe/player/ui/PopupPlayerUi.java
@@ -69,11 +69,9 @@ public PopupPlayerUi(@NonNull final Player player,
@Override
public void setupAfterIntent() {
- setupElementsVisibility();
- binding.getRoot().setVisibility(View.VISIBLE);
+ super.setupAfterIntent();
initPopup();
initPopupCloseOverlay();
- binding.playPauseButton.requestFocus();
}
@Override
@@ -103,6 +101,7 @@ private void initPopup() {
binding.loadingPanel.setMinimumHeight(popupLayoutParams.height);
windowManager.addView(binding.getRoot(), popupLayoutParams);
+ setupVideoSurfaceIfNeeded(); // now there is a parent, we can setup video surface
// Popup doesn't have aspectRatio selector, using FIT automatically
setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIT);
@@ -304,25 +303,23 @@ public boolean isPopupClosing() {
}
public void removePopupFromView() {
- if (windowManager != null) {
- // wrap in try-catch since it could sometimes generate errors randomly
- try {
- if (popupHasParent()) {
- windowManager.removeView(binding.getRoot());
- }
- } catch (final IllegalArgumentException e) {
- Log.w(TAG, "Failed to remove popup from window manager", e);
+ // wrap in try-catch since it could sometimes generate errors randomly
+ try {
+ if (popupHasParent()) {
+ windowManager.removeView(binding.getRoot());
}
+ } catch (final IllegalArgumentException e) {
+ Log.w(TAG, "Failed to remove popup from window manager", e);
+ }
- try {
- final boolean closeOverlayHasParent = closeOverlayBinding != null
- && closeOverlayBinding.getRoot().getParent() != null;
- if (closeOverlayHasParent) {
- windowManager.removeView(closeOverlayBinding.getRoot());
- }
- } catch (final IllegalArgumentException e) {
- Log.w(TAG, "Failed to remove popup overlay from window manager", e);
+ try {
+ final boolean closeOverlayHasParent = closeOverlayBinding != null
+ && closeOverlayBinding.getRoot().getParent() != null;
+ if (closeOverlayHasParent) {
+ windowManager.removeView(closeOverlayBinding.getRoot());
}
+ } catch (final IllegalArgumentException e) {
+ Log.w(TAG, "Failed to remove popup overlay from window manager", e);
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java
index 99ecb5540a1..24cdb8908bb 100644
--- a/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java
+++ b/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java
@@ -32,7 +32,6 @@
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
-import android.view.Surface;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
@@ -107,6 +106,7 @@ public abstract class VideoPlayerUi extends PlayerUi
protected PlayerBinding binding;
private final Handler controlsVisibilityHandler = new Handler();
@Nullable private SurfaceHolderCallback surfaceHolderCallback;
+ boolean surfaceIsSetup = false;
@Nullable private Bitmap thumbnail = null;
@@ -130,6 +130,7 @@ public abstract class VideoPlayerUi extends PlayerUi
private GestureDetector gestureDetector;
private BasePlayerGestureListener playerGestureListener;
+ @Nullable private View.OnLayoutChangeListener onLayoutChangeListener = null;
@NonNull private final SeekbarPreviewThumbnailHolder seekbarPreviewThumbnailHolder =
new SeekbarPreviewThumbnailHolder();
@@ -138,6 +139,7 @@ public VideoPlayerUi(@NonNull final Player player,
@NonNull final PlayerBinding playerBinding) {
super(player);
binding = playerBinding;
+ setupFromView();
}
@@ -222,8 +224,8 @@ protected void initListeners() {
// PlaybackControlRoot already consumed window insets but we should pass them to
// player_overlays and fast_seek_overlay too. Without it they will be off-centered.
- binding.playbackControlRoot.addOnLayoutChangeListener(
- (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
+ onLayoutChangeListener
+ = (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
binding.playerOverlays.setPadding(
v.getPaddingLeft(),
v.getPaddingTop(),
@@ -240,7 +242,43 @@ protected void initListeners() {
fastSeekParams.topMargin = -v.getPaddingBottom();
fastSeekParams.rightMargin = -v.getPaddingLeft();
fastSeekParams.bottomMargin = -v.getPaddingTop();
- });
+ };
+ binding.playbackControlRoot.addOnLayoutChangeListener(onLayoutChangeListener);
+ }
+
+ protected void deinitListeners() {
+ binding.qualityTextView.setOnClickListener(null);
+ binding.playbackSpeed.setOnClickListener(null);
+ binding.playbackSeekBar.setOnSeekBarChangeListener(null);
+ binding.captionTextView.setOnClickListener(null);
+ binding.resizeTextView.setOnClickListener(null);
+ binding.playbackLiveSync.setOnClickListener(null);
+
+ binding.getRoot().setOnTouchListener(null);
+ playerGestureListener = null;
+ gestureDetector = null;
+
+ binding.repeatButton.setOnClickListener(null);
+ binding.shuffleButton.setOnClickListener(null);
+
+ binding.playPauseButton.setOnClickListener(null);
+ binding.playPreviousButton.setOnClickListener(null);
+ binding.playNextButton.setOnClickListener(null);
+
+ binding.moreOptionsButton.setOnClickListener(null);
+ binding.moreOptionsButton.setOnLongClickListener(null);
+ binding.share.setOnClickListener(null);
+ binding.share.setOnLongClickListener(null);
+ binding.fullScreenButton.setOnClickListener(null);
+ binding.screenRotationButton.setOnClickListener(null);
+ binding.playWithKodi.setOnClickListener(null);
+ binding.openInBrowser.setOnClickListener(null);
+ binding.playerCloseButton.setOnClickListener(null);
+ binding.switchMute.setOnClickListener(null);
+
+ ViewCompat.setOnApplyWindowInsetsListener(binding.itemsListPanel, null);
+
+ binding.playbackControlRoot.removeOnLayoutChangeListener(onLayoutChangeListener);
}
/**
@@ -304,18 +342,25 @@ public void seek(final boolean forward) {
playerGestureListener.doubleTapControls(binding.fastSeekOverlay);
}
+ public void deinitPlayerSeekOverlay() {
+ binding.fastSeekOverlay
+ .seekSecondsSupplier(null)
+ .performListener(null);
+ }
+
@Override
public void setupAfterIntent() {
super.setupAfterIntent();
setupElementsVisibility();
setupElementsSize(context.getResources());
+ binding.getRoot().setVisibility(View.VISIBLE);
+ binding.playPauseButton.requestFocus();
}
@Override
public void initPlayer() {
super.initPlayer();
- setupVideoSurface();
- setupFromView();
+ setupVideoSurfaceIfNeeded();
}
@Override
@@ -331,7 +376,7 @@ public void initPlayback() {
@Override
public void destroyPlayer() {
super.destroyPlayer();
- cleanupVideoSurface();
+ clearVideoSurface();
}
@Override
@@ -340,6 +385,8 @@ public void destroy() {
if (binding != null) {
binding.endScreen.setImageBitmap(null);
}
+ deinitPlayerSeekOverlay();
+ deinitListeners();
}
protected void setupElementsVisibility() {
@@ -1470,40 +1517,50 @@ public void onVideoSizeChanged(@NonNull VideoSize videoSize) {
// SurfaceHolderCallback helpers
//////////////////////////////////////////////////////////////////////////*/
//region SurfaceHolderCallback helpers
- private void setupVideoSurface() {
- // make sure there is nothing left over from previous calls
- cleanupVideoSurface();
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // >=API23
- surfaceHolderCallback = new SurfaceHolderCallback(context, player.getExoPlayer());
- binding.surfaceView.getHolder().addCallback(surfaceHolderCallback);
- final Surface surface = binding.surfaceView.getHolder().getSurface();
-
- // ensure player is using an unreleased surface, which the surfaceView might not be
- // when starting playback on background or during player switching
- if (surface.isValid()) {
- // initially set the surface manually otherwise
- // onRenderedFirstFrame() will not be called
- player.getExoPlayer().setVideoSurface(surface);
+
+ /**
+ * Connects the video surface to the exo player. This can be called anytime without the risk for
+ * issues to occur, since the player will run just fine when no surface is connected. Therefore
+ * the video surface will be setup only when all of these conditions are true: it is not already
+ * setup (this just prevents wasting resources to setup the surface again), there is an exo
+ * player, the root view is attached to a parent and the surface view is valid/unreleased (the
+ * latter two conditions prevent "The surface has been released" errors). So this function can
+ * be called many times and even while the UI is in unready states.
+ */
+ public void setupVideoSurfaceIfNeeded() {
+ if (!surfaceIsSetup && player.getExoPlayer() != null
+ && binding.getRoot().getParent() != null) {
+ // make sure there is nothing left over from previous calls
+ clearVideoSurface();
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // >=API23
+ surfaceHolderCallback = new SurfaceHolderCallback(context, player.getExoPlayer());
+ binding.surfaceView.getHolder().addCallback(surfaceHolderCallback);
+
+ // ensure player is using an unreleased surface, which the surfaceView might not be
+ // when starting playback on background or during player switching
+ if (binding.surfaceView.getHolder().getSurface().isValid()) {
+ // initially set the surface manually otherwise
+ // onRenderedFirstFrame() will not be called
+ player.getExoPlayer().setVideoSurfaceHolder(binding.surfaceView.getHolder());
+ }
+ } else {
+ player.getExoPlayer().setVideoSurfaceView(binding.surfaceView);
}
- } else {
- player.getExoPlayer().setVideoSurfaceView(binding.surfaceView);
+ surfaceIsSetup = true;
}
}
- private void cleanupVideoSurface() {
- final Optional exoPlayer = Optional.ofNullable(player.getExoPlayer());
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // >=API23
- if (surfaceHolderCallback != null) {
- binding.surfaceView.getHolder().removeCallback(surfaceHolderCallback);
- surfaceHolderCallback.release();
- surfaceHolderCallback = null;
- }
- exoPlayer.ifPresent(simpleExoPlayer -> simpleExoPlayer.setVideoSurface(null));
- } else {
- exoPlayer.ifPresent(simpleExoPlayer -> simpleExoPlayer.setVideoSurfaceView(null));
+ private void clearVideoSurface() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M // >=API23
+ && surfaceHolderCallback != null) {
+ binding.surfaceView.getHolder().removeCallback(surfaceHolderCallback);
+ surfaceHolderCallback.release();
+ surfaceHolderCallback = null;
}
+ Optional.ofNullable(player.getExoPlayer()).ifPresent(ExoPlayer::clearVideoSurface);
+ surfaceIsSetup = false;
}
//endregion
diff --git a/app/src/main/java/org/schabi/newpipe/views/player/PlayerFastSeekOverlay.kt b/app/src/main/java/org/schabi/newpipe/views/player/PlayerFastSeekOverlay.kt
index cbba0a75b54..d0782e1a18d 100644
--- a/app/src/main/java/org/schabi/newpipe/views/player/PlayerFastSeekOverlay.kt
+++ b/app/src/main/java/org/schabi/newpipe/views/player/PlayerFastSeekOverlay.kt
@@ -38,14 +38,14 @@ class PlayerFastSeekOverlay(context: Context, attrs: AttributeSet?) :
private var performListener: PerformListener? = null
- fun performListener(listener: PerformListener) = apply {
+ fun performListener(listener: PerformListener?) = apply {
performListener = listener
}
private var seekSecondsSupplier: () -> Int = { 0 }
- fun seekSecondsSupplier(supplier: () -> Int) = apply {
- seekSecondsSupplier = supplier
+ fun seekSecondsSupplier(supplier: (() -> Int)?) = apply {
+ seekSecondsSupplier = supplier ?: { 0 }
}
// Indicates whether this (double) tap is the first of a series
From 0bba1d95dee627843bc97224e6e04260931fa9a8 Mon Sep 17 00:00:00 2001
From: Stypox
Date: Thu, 14 Apr 2022 18:14:28 +0200
Subject: [PATCH 123/240] Move all notification-related calls to
NotificationPlayerUi
---
.../newpipe/player/NotificationUtil.java | 93 +++++++--------
.../org/schabi/newpipe/player/Player.java | 60 ++--------
.../schabi/newpipe/player/PlayerService.java | 4 -
.../player/ui/NotificationPlayerUi.java | 107 +++++++++++++++++-
4 files changed, 154 insertions(+), 110 deletions(-)
diff --git a/app/src/main/java/org/schabi/newpipe/player/NotificationUtil.java b/app/src/main/java/org/schabi/newpipe/player/NotificationUtil.java
index f5caf2c7974..e88defe7f42 100644
--- a/app/src/main/java/org/schabi/newpipe/player/NotificationUtil.java
+++ b/app/src/main/java/org/schabi/newpipe/player/NotificationUtil.java
@@ -45,22 +45,16 @@ public final class NotificationUtil {
private static final boolean DEBUG = Player.DEBUG;
private static final int NOTIFICATION_ID = 123789;
- @Nullable private static NotificationUtil instance = null;
-
@NotificationConstants.Action
private final int[] notificationSlots = NotificationConstants.SLOT_DEFAULTS.clone();
private NotificationManagerCompat notificationManager;
private NotificationCompat.Builder notificationBuilder;
- private NotificationUtil() {
- }
+ private Player player;
- public static NotificationUtil getInstance() {
- if (instance == null) {
- instance = new NotificationUtil();
- }
- return instance;
+ public NotificationUtil(final Player player) {
+ this.player = player;
}
@@ -71,20 +65,18 @@ public static NotificationUtil getInstance() {
/**
* Creates the notification if it does not exist already and recreates it if forceRecreate is
* true. Updates the notification with the data in the player.
- * @param player the player currently open, to take data from
* @param forceRecreate whether to force the recreation of the notification even if it already
* exists
*/
- synchronized void createNotificationIfNeededAndUpdate(final Player player,
- final boolean forceRecreate) {
+ public synchronized void createNotificationIfNeededAndUpdate(final boolean forceRecreate) {
if (forceRecreate || notificationBuilder == null) {
- notificationBuilder = createNotification(player);
+ notificationBuilder = createNotification();
}
- updateNotification(player);
+ updateNotification();
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
}
- private synchronized NotificationCompat.Builder createNotification(final Player player) {
+ private synchronized NotificationCompat.Builder createNotification() {
if (DEBUG) {
Log.d(TAG, "createNotification()");
}
@@ -93,7 +85,7 @@ private synchronized NotificationCompat.Builder createNotification(final Player
new NotificationCompat.Builder(player.getContext(),
player.getContext().getString(R.string.notification_channel_id));
- initializeNotificationSlots(player);
+ initializeNotificationSlots();
// count the number of real slots, to make sure compact slots indices are not out of bound
int nonNothingSlotCount = 5;
@@ -132,9 +124,8 @@ private synchronized NotificationCompat.Builder createNotification(final Player
/**
* Updates the notification builder and the button icons depending on the playback state.
- * @param player the player currently open, to take data from
*/
- private synchronized void updateNotification(final Player player) {
+ private synchronized void updateNotification() {
if (DEBUG) {
Log.d(TAG, "updateNotification()");
}
@@ -145,17 +136,17 @@ private synchronized void updateNotification(final Player player) {
notificationBuilder.setContentTitle(player.getVideoTitle());
notificationBuilder.setContentText(player.getUploaderName());
notificationBuilder.setTicker(player.getVideoTitle());
- updateActions(notificationBuilder, player);
+ updateActions(notificationBuilder);
final boolean showThumbnail = player.getPrefs().getBoolean(
player.getContext().getString(R.string.show_thumbnail_key), true);
if (showThumbnail) {
- setLargeIcon(notificationBuilder, player);
+ setLargeIcon(notificationBuilder);
}
}
@SuppressLint("RestrictedApi")
- boolean shouldUpdateBufferingSlot() {
+ public boolean shouldUpdateBufferingSlot() {
if (notificationBuilder == null) {
// if there is no notification active, there is no point in updating it
return false;
@@ -173,22 +164,22 @@ boolean shouldUpdateBufferingSlot() {
}
- public void createNotificationAndStartForeground(final Player player, final Service service) {
+ public void createNotificationAndStartForeground() {
if (notificationBuilder == null) {
- notificationBuilder = createNotification(player);
+ notificationBuilder = createNotification();
}
- updateNotification(player);
+ updateNotification();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- service.startForeground(NOTIFICATION_ID, notificationBuilder.build(),
+ player.getService().startForeground(NOTIFICATION_ID, notificationBuilder.build(),
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
} else {
- service.startForeground(NOTIFICATION_ID, notificationBuilder.build());
+ player.getService().startForeground(NOTIFICATION_ID, notificationBuilder.build());
}
}
- void cancelNotificationAndStopForeground(final Service service) {
- ServiceCompat.stopForeground(service, ServiceCompat.STOP_FOREGROUND_REMOVE);
+ public void cancelNotificationAndStopForeground() {
+ ServiceCompat.stopForeground(player.getService(), ServiceCompat.STOP_FOREGROUND_REMOVE);
if (notificationManager != null) {
notificationManager.cancel(NOTIFICATION_ID);
@@ -202,7 +193,7 @@ void cancelNotificationAndStopForeground(final Service service) {
// ACTIONS
/////////////////////////////////////////////////////
- private void initializeNotificationSlots(final Player player) {
+ private void initializeNotificationSlots() {
for (int i = 0; i < 5; ++i) {
notificationSlots[i] = player.getPrefs().getInt(
player.getContext().getString(NotificationConstants.SLOT_PREF_KEYS[i]),
@@ -211,7 +202,7 @@ private void initializeNotificationSlots(final Player player) {
}
@SuppressLint("RestrictedApi")
- private void updateActions(final NotificationCompat.Builder builder, final Player player) {
+ private void updateActions(final NotificationCompat.Builder builder) {
builder.mActions.clear();
for (int i = 0; i < 5; ++i) {
addAction(builder, player, notificationSlots[i]);
@@ -221,7 +212,7 @@ private void updateActions(final NotificationCompat.Builder builder, final Playe
private void addAction(final NotificationCompat.Builder builder,
final Player player,
@NotificationConstants.Action final int slot) {
- final NotificationCompat.Action action = getAction(player, slot);
+ final NotificationCompat.Action action = getAction(slot);
if (action != null) {
builder.addAction(action);
}
@@ -229,41 +220,40 @@ private void addAction(final NotificationCompat.Builder builder,
@Nullable
private NotificationCompat.Action getAction(
- final Player player,
@NotificationConstants.Action final int selectedAction) {
final int baseActionIcon = NotificationConstants.ACTION_ICONS[selectedAction];
switch (selectedAction) {
case NotificationConstants.PREVIOUS:
- return getAction(player, baseActionIcon,
+ return getAction(baseActionIcon,
R.string.exo_controls_previous_description, ACTION_PLAY_PREVIOUS);
case NotificationConstants.NEXT:
- return getAction(player, baseActionIcon,
+ return getAction(baseActionIcon,
R.string.exo_controls_next_description, ACTION_PLAY_NEXT);
case NotificationConstants.REWIND:
- return getAction(player, baseActionIcon,
+ return getAction(baseActionIcon,
R.string.exo_controls_rewind_description, ACTION_FAST_REWIND);
case NotificationConstants.FORWARD:
- return getAction(player, baseActionIcon,
+ return getAction(baseActionIcon,
R.string.exo_controls_fastforward_description, ACTION_FAST_FORWARD);
case NotificationConstants.SMART_REWIND_PREVIOUS:
if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) {
- return getAction(player, R.drawable.exo_notification_previous,
+ return getAction(R.drawable.exo_notification_previous,
R.string.exo_controls_previous_description, ACTION_PLAY_PREVIOUS);
} else {
- return getAction(player, R.drawable.exo_controls_rewind,
+ return getAction(R.drawable.exo_controls_rewind,
R.string.exo_controls_rewind_description, ACTION_FAST_REWIND);
}
case NotificationConstants.SMART_FORWARD_NEXT:
if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) {
- return getAction(player, R.drawable.exo_notification_next,
+ return getAction(R.drawable.exo_notification_next,
R.string.exo_controls_next_description, ACTION_PLAY_NEXT);
} else {
- return getAction(player, R.drawable.exo_controls_fastforward,
+ return getAction(R.drawable.exo_controls_fastforward,
R.string.exo_controls_fastforward_description, ACTION_FAST_FORWARD);
}
@@ -279,42 +269,42 @@ private NotificationCompat.Action getAction(
case NotificationConstants.PLAY_PAUSE:
if (player.getCurrentState() == Player.STATE_COMPLETED) {
- return getAction(player, R.drawable.ic_replay,
+ return getAction(R.drawable.ic_replay,
R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE);
} else if (player.isPlaying()
|| player.getCurrentState() == Player.STATE_PREFLIGHT
|| player.getCurrentState() == Player.STATE_BLOCKED
|| player.getCurrentState() == Player.STATE_BUFFERING) {
- return getAction(player, R.drawable.exo_notification_pause,
+ return getAction(R.drawable.exo_notification_pause,
R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE);
} else {
- return getAction(player, R.drawable.exo_notification_play,
+ return getAction(R.drawable.exo_notification_play,
R.string.exo_controls_play_description, ACTION_PLAY_PAUSE);
}
case NotificationConstants.REPEAT:
if (player.getRepeatMode() == REPEAT_MODE_ALL) {
- return getAction(player, R.drawable.exo_media_action_repeat_all,
+ return getAction(R.drawable.exo_media_action_repeat_all,
R.string.exo_controls_repeat_all_description, ACTION_REPEAT);
} else if (player.getRepeatMode() == REPEAT_MODE_ONE) {
- return getAction(player, R.drawable.exo_media_action_repeat_one,
+ return getAction(R.drawable.exo_media_action_repeat_one,
R.string.exo_controls_repeat_one_description, ACTION_REPEAT);
} else /* player.getRepeatMode() == REPEAT_MODE_OFF */ {
- return getAction(player, R.drawable.exo_media_action_repeat_off,
+ return getAction(R.drawable.exo_media_action_repeat_off,
R.string.exo_controls_repeat_off_description, ACTION_REPEAT);
}
case NotificationConstants.SHUFFLE:
if (player.getPlayQueue() != null && player.getPlayQueue().isShuffled()) {
- return getAction(player, R.drawable.exo_controls_shuffle_on,
+ return getAction(R.drawable.exo_controls_shuffle_on,
R.string.exo_controls_shuffle_on_description, ACTION_SHUFFLE);
} else {
- return getAction(player, R.drawable.exo_controls_shuffle_off,
+ return getAction(R.drawable.exo_controls_shuffle_off,
R.string.exo_controls_shuffle_off_description, ACTION_SHUFFLE);
}
case NotificationConstants.CLOSE:
- return getAction(player, R.drawable.ic_close,
+ return getAction(R.drawable.ic_close,
R.string.close, ACTION_CLOSE);
case NotificationConstants.NOTHING:
@@ -324,8 +314,7 @@ private NotificationCompat.Action getAction(
}
}
- private NotificationCompat.Action getAction(final Player player,
- @DrawableRes final int drawable,
+ private NotificationCompat.Action getAction(@DrawableRes final int drawable,
@StringRes final int title,
final String intentAction) {
return new NotificationCompat.Action(drawable, player.getContext().getString(title),
@@ -353,7 +342,7 @@ private Intent getIntentForNotification(final Player player) {
// BITMAP
/////////////////////////////////////////////////////
- private void setLargeIcon(final NotificationCompat.Builder builder, final Player player) {
+ private void setLargeIcon(final NotificationCompat.Builder builder) {
final boolean scaleImageToSquareAspectRatio = player.getPrefs().getBoolean(
player.getContext().getString(R.string.scale_to_square_image_in_notifications_key),
false);
diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java
index 78e93970c70..e2732f4d01f 100644
--- a/app/src/main/java/org/schabi/newpipe/player/Player.java
+++ b/app/src/main/java/org/schabi/newpipe/player/Player.java
@@ -38,7 +38,6 @@
import static org.schabi.newpipe.player.PlayerService.ACTION_RECREATE_NOTIFICATION;
import static org.schabi.newpipe.player.PlayerService.ACTION_REPEAT;
import static org.schabi.newpipe.player.PlayerService.ACTION_SHUFFLE;
-import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE;
import static org.schabi.newpipe.player.helper.PlayerHelper.isPlaybackResumeEnabled;
import static org.schabi.newpipe.player.helper.PlayerHelper.nextRepeatMode;
import static org.schabi.newpipe.player.helper.PlayerHelper.retrievePlaybackParametersFromPrefs;
@@ -620,7 +619,7 @@ public void setRecovery() {
}
private void setRecovery(final int queuePos, final long windowPos) {
- if (playQueue.size() <= queuePos) {
+ if (playQueue == null || playQueue.size() <= queuePos) {
return;
}
@@ -735,9 +734,6 @@ private void onBroadcastReceived(final Intent intent) {
case ACTION_SHUFFLE:
toggleShuffleModeEnabled();
break;
- case ACTION_RECREATE_NOTIFICATION:
- NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, true);
- break;
case Intent.ACTION_CONFIGURATION_CHANGED:
assureCorrectAppLanguage(service);
if (DEBUG) {
@@ -797,8 +793,6 @@ public void onBitmapLoaded(final Bitmap bitmap, final Picasso.LoadedFrom from) {
}
currentThumbnail = bitmap;
- NotificationUtil.getInstance()
- .createNotificationIfNeededAndUpdate(Player.this, false);
// there is a new thumbnail, so changed the end screen thumbnail, too.
UIs.call(playerUi -> playerUi.onThumbnailLoaded(bitmap));
}
@@ -807,8 +801,7 @@ public void onBitmapLoaded(final Bitmap bitmap, final Picasso.LoadedFrom from) {
public void onBitmapFailed(final Exception e, final Drawable errorDrawable) {
Log.e(TAG, "Thumbnail - onBitmapFailed() called: url = [" + url + "]", e);
currentThumbnail = null;
- NotificationUtil.getInstance()
- .createNotificationIfNeededAndUpdate(Player.this, false);
+ UIs.call(playerUi -> playerUi.onThumbnailLoaded(null));
}
@Override
@@ -1082,8 +1075,6 @@ private void onBlocked() {
}
UIs.call(PlayerUi::onBlocked);
-
- NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
}
private void onPlaying() {
@@ -1095,8 +1086,6 @@ private void onPlaying() {
}
UIs.call(PlayerUi::onPlaying);
-
- NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
}
private void onBuffering() {
@@ -1105,10 +1094,6 @@ private void onBuffering() {
}
UIs.call(PlayerUi::onBuffering);
-
- if (NotificationUtil.getInstance().shouldUpdateBufferingSlot()) {
- NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
- }
}
private void onPaused() {
@@ -1121,24 +1106,13 @@ private void onPaused() {
}
UIs.call(PlayerUi::onPaused);
-
- // Remove running notification when user does not want minimization to background or popup
- if (PlayerHelper.getMinimizeOnExitAction(context) == MINIMIZE_ON_EXIT_MODE_NONE
- && videoPlayerSelected()) {
- NotificationUtil.getInstance().cancelNotificationAndStopForeground(service);
- } else {
- NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
- }
}
private void onPausedSeek() {
if (DEBUG) {
Log.d(TAG, "onPausedSeek() called");
}
-
UIs.call(PlayerUi::onPausedSeek);
-
- NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
}
private void onCompleted() {
@@ -1150,7 +1124,6 @@ private void onCompleted() {
}
UIs.call(PlayerUi::onCompleted);
- NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
if (playQueue.getIndex() < playQueue.size() - 1) {
playQueue.offsetIndex(+1);
@@ -1190,7 +1163,7 @@ public void onRepeatModeChanged(@RepeatMode final int repeatMode) {
+ "repeatMode = [" + repeatMode + "]");
}
UIs.call(playerUi -> playerUi.onRepeatModeChanged(repeatMode));
- onShuffleOrRepeatModeChanged();
+ notifyPlaybackUpdateToListeners();
}
@Override
@@ -1209,7 +1182,7 @@ public void onShuffleModeEnabledChanged(final boolean shuffleModeEnabled) {
}
UIs.call(playerUi -> playerUi.onShuffleModeEnabledChanged(shuffleModeEnabled));
- onShuffleOrRepeatModeChanged();
+ notifyPlaybackUpdateToListeners();
}
public void toggleShuffleModeEnabled() {
@@ -1217,11 +1190,6 @@ public void toggleShuffleModeEnabled() {
simpleExoPlayer.setShuffleModeEnabled(!simpleExoPlayer.getShuffleModeEnabled());
}
}
-
- private void onShuffleOrRepeatModeChanged() {
- notifyPlaybackUpdateToListeners();
- NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
- }
//endregion
@@ -1806,12 +1774,15 @@ public void saveStreamProgressStateCompleted() {
//////////////////////////////////////////////////////////////////////////*/
//region Metadata
- private void onMetadataChanged(@NonNull final StreamInfo info) {
+ private void updateMetadataWith(@NonNull final StreamInfo info) {
if (DEBUG) {
Log.d(TAG, "Playback - onMetadataChanged() called, playing: " + info.getName());
}
+ if (exoPlayerIsNull()) {
+ return;
+ }
- UIs.call(playerUi -> playerUi.onMetadataChanged(info));
+ maybeAutoQueueNextStream(info);
initThumbnail(info.getThumbnailUrl());
registerStreamViewed();
@@ -1826,17 +1797,7 @@ private void onMetadataChanged(@NonNull final StreamInfo info) {
);
notifyMetadataUpdateToListeners();
- NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
- }
-
- private void updateMetadataWith(@NonNull final StreamInfo streamInfo) {
- if (exoPlayerIsNull()) {
- return;
- }
-
- maybeAutoQueueNextStream(streamInfo);
- onMetadataChanged(streamInfo);
- NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, true);
+ UIs.call(playerUi -> playerUi.onMetadataChanged(info));
}
@NonNull
@@ -1925,7 +1886,6 @@ public void selectQueueItem(final PlayQueueItem item) {
public void onPlayQueueEdited() {
notifyPlaybackUpdateToListeners();
UIs.call(PlayerUi::onPlayQueueEdited);
- NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
}
@Override // own playback listener
diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayerService.java b/app/src/main/java/org/schabi/newpipe/player/PlayerService.java
index cf83dc5c277..7bf918c7308 100644
--- a/app/src/main/java/org/schabi/newpipe/player/PlayerService.java
+++ b/app/src/main/java/org/schabi/newpipe/player/PlayerService.java
@@ -88,9 +88,6 @@ public void onCreate() {
ThemeHelper.setTheme(this);
player = new Player(this);
- /*final MainPlayerUi mainPlayerUi = new MainPlayerUi(player,
- PlayerBinding.inflate(LayoutInflater.from(this)));
- player.UIs().add(mainPlayerUi);*/
}
@Override
@@ -159,7 +156,6 @@ private void cleanup() {
}
public void stopService() {
- NotificationUtil.getInstance().cancelNotificationAndStopForeground(this);
cleanup();
stopSelf();
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/NotificationPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/NotificationPlayerUi.java
index 40c83c6c779..5736eca3bbd 100644
--- a/app/src/main/java/org/schabi/newpipe/player/ui/NotificationPlayerUi.java
+++ b/app/src/main/java/org/schabi/newpipe/player/ui/NotificationPlayerUi.java
@@ -1,26 +1,125 @@
package org.schabi.newpipe.player.ui;
+import static org.schabi.newpipe.player.PlayerService.ACTION_RECREATE_NOTIFICATION;
+import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE;
+
+import android.content.Intent;
+import android.graphics.Bitmap;
+
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.google.android.exoplayer2.Player.RepeatMode;
+import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.player.NotificationUtil;
import org.schabi.newpipe.player.Player;
+import org.schabi.newpipe.player.helper.PlayerHelper;
public final class NotificationPlayerUi extends PlayerUi {
- boolean foregroundNotificationAlreadyCreated = false;
+ private boolean foregroundNotificationAlreadyCreated = false;
+ private final NotificationUtil notificationUtil;
public NotificationPlayerUi(@NonNull final Player player) {
super(player);
+ notificationUtil = new NotificationUtil(player);
}
@Override
public void initPlayer() {
super.initPlayer();
if (!foregroundNotificationAlreadyCreated) {
- NotificationUtil.getInstance()
- .createNotificationAndStartForeground(player, player.getService());
+ notificationUtil.createNotificationAndStartForeground();
foregroundNotificationAlreadyCreated = true;
}
}
- // TODO TODO on destroy remove foreground
+ @Override
+ public void destroy() {
+ super.destroy();
+ notificationUtil.cancelNotificationAndStopForeground();
+ }
+
+ @Override
+ public void onThumbnailLoaded(@Nullable final Bitmap bitmap) {
+ super.onThumbnailLoaded(bitmap);
+ notificationUtil.createNotificationIfNeededAndUpdate(false);
+ }
+
+ @Override
+ public void onBlocked() {
+ super.onBlocked();
+ notificationUtil.createNotificationIfNeededAndUpdate(false);
+ }
+
+ @Override
+ public void onPlaying() {
+ super.onPlaying();
+ notificationUtil.createNotificationIfNeededAndUpdate(false);
+ }
+
+ @Override
+ public void onBuffering() {
+ super.onBuffering();
+ if (notificationUtil.shouldUpdateBufferingSlot()) {
+ notificationUtil.createNotificationIfNeededAndUpdate(false);
+ }
+ }
+
+ @Override
+ public void onPaused() {
+ super.onPaused();
+
+ // Remove running notification when user does not want minimization to background or popup
+ if (PlayerHelper.getMinimizeOnExitAction(context) == MINIMIZE_ON_EXIT_MODE_NONE
+ && player.videoPlayerSelected()) {
+ notificationUtil.cancelNotificationAndStopForeground();
+ } else {
+ notificationUtil.createNotificationIfNeededAndUpdate(false);
+ }
+ }
+
+ @Override
+ public void onPausedSeek() {
+ super.onPausedSeek();
+ notificationUtil.createNotificationIfNeededAndUpdate(false);
+ }
+
+ @Override
+ public void onCompleted() {
+ super.onCompleted();
+ notificationUtil.createNotificationIfNeededAndUpdate(false);
+ }
+
+ @Override
+ public void onRepeatModeChanged(@RepeatMode final int repeatMode) {
+ super.onRepeatModeChanged(repeatMode);
+ notificationUtil.createNotificationIfNeededAndUpdate(false);
+ }
+
+ @Override
+ public void onShuffleModeEnabledChanged(final boolean shuffleModeEnabled) {
+ super.onShuffleModeEnabledChanged(shuffleModeEnabled);
+ notificationUtil.createNotificationIfNeededAndUpdate(false);
+ }
+
+ @Override
+ public void onBroadcastReceived(final Intent intent) {
+ super.onBroadcastReceived(intent);
+ if (ACTION_RECREATE_NOTIFICATION.equals(intent.getAction())) {
+ notificationUtil.createNotificationIfNeededAndUpdate(true);
+ }
+ }
+
+ @Override
+ public void onMetadataChanged(@NonNull final StreamInfo info) {
+ super.onMetadataChanged(info);
+ notificationUtil.createNotificationIfNeededAndUpdate(true);
+ }
+
+ @Override
+ public void onPlayQueueEdited() {
+ super.onPlayQueueEdited();
+ notificationUtil.createNotificationIfNeededAndUpdate(false);
+ }
}
From 90a89f8ca555433b1e8f9f9d2713d0f7667060be Mon Sep 17 00:00:00 2001
From: Stypox
Date: Thu, 14 Apr 2022 18:40:55 +0200
Subject: [PATCH 124/240] Move player-notification files into their package
---
.../org/schabi/newpipe/player/Player.java | 20 ++++++------
.../schabi/newpipe/player/PlayerService.java | 25 ---------------
.../NotificationConstants.java | 32 +++++++++++++++++--
.../NotificationPlayerUi.java | 6 ++--
.../{ => notification}/NotificationUtil.java | 25 +++++++--------
.../newpipe/player/ui/MainPlayerUi.java | 2 +-
.../custom/NotificationActionsPreference.java | 6 ++--
7 files changed, 60 insertions(+), 56 deletions(-)
rename app/src/main/java/org/schabi/newpipe/player/{ => notification}/NotificationConstants.java (82%)
rename app/src/main/java/org/schabi/newpipe/player/{ui => notification}/NotificationPlayerUi.java (94%)
rename app/src/main/java/org/schabi/newpipe/player/{ => notification}/NotificationUtil.java (94%)
diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java
index e2732f4d01f..55600b95698 100644
--- a/app/src/main/java/org/schabi/newpipe/player/Player.java
+++ b/app/src/main/java/org/schabi/newpipe/player/Player.java
@@ -29,21 +29,21 @@
import static com.google.android.exoplayer2.Player.RepeatMode;
import static org.schabi.newpipe.extractor.ServiceList.YouTube;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
-import static org.schabi.newpipe.player.PlayerService.ACTION_CLOSE;
-import static org.schabi.newpipe.player.PlayerService.ACTION_FAST_FORWARD;
-import static org.schabi.newpipe.player.PlayerService.ACTION_FAST_REWIND;
-import static org.schabi.newpipe.player.PlayerService.ACTION_PLAY_NEXT;
-import static org.schabi.newpipe.player.PlayerService.ACTION_PLAY_PAUSE;
-import static org.schabi.newpipe.player.PlayerService.ACTION_PLAY_PREVIOUS;
-import static org.schabi.newpipe.player.PlayerService.ACTION_RECREATE_NOTIFICATION;
-import static org.schabi.newpipe.player.PlayerService.ACTION_REPEAT;
-import static org.schabi.newpipe.player.PlayerService.ACTION_SHUFFLE;
import static org.schabi.newpipe.player.helper.PlayerHelper.isPlaybackResumeEnabled;
import static org.schabi.newpipe.player.helper.PlayerHelper.nextRepeatMode;
import static org.schabi.newpipe.player.helper.PlayerHelper.retrievePlaybackParametersFromPrefs;
import static org.schabi.newpipe.player.helper.PlayerHelper.retrievePlayerTypeFromIntent;
import static org.schabi.newpipe.player.helper.PlayerHelper.retrieveSeekDurationFromPreferences;
import static org.schabi.newpipe.player.helper.PlayerHelper.savePlaybackParametersToPrefs;
+import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_CLOSE;
+import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_FAST_FORWARD;
+import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_FAST_REWIND;
+import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_NEXT;
+import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_PAUSE;
+import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_PREVIOUS;
+import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_RECREATE_NOTIFICATION;
+import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_REPEAT;
+import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_SHUFFLE;
import static org.schabi.newpipe.util.ListHelper.getPopupResolutionIndex;
import static org.schabi.newpipe.util.ListHelper.getResolutionIndex;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
@@ -125,7 +125,7 @@
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver;
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver.SourceType;
import org.schabi.newpipe.player.ui.MainPlayerUi;
-import org.schabi.newpipe.player.ui.NotificationPlayerUi;
+import org.schabi.newpipe.player.notification.NotificationPlayerUi;
import org.schabi.newpipe.player.ui.PlayerUi;
import org.schabi.newpipe.player.ui.PlayerUiList;
import org.schabi.newpipe.player.ui.PopupPlayerUi;
diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayerService.java b/app/src/main/java/org/schabi/newpipe/player/PlayerService.java
index 7bf918c7308..b5014eeed09 100644
--- a/app/src/main/java/org/schabi/newpipe/player/PlayerService.java
+++ b/app/src/main/java/org/schabi/newpipe/player/PlayerService.java
@@ -28,8 +28,6 @@
import android.os.IBinder;
import android.util.Log;
-import org.schabi.newpipe.App;
-import org.schabi.newpipe.player.ui.VideoPlayerUi;
import org.schabi.newpipe.util.ThemeHelper;
@@ -52,29 +50,6 @@ public enum PlayerType {
POPUP
}
- /*//////////////////////////////////////////////////////////////////////////
- // Notification
- //////////////////////////////////////////////////////////////////////////*/
-
- static final String ACTION_CLOSE
- = App.PACKAGE_NAME + ".player.MainPlayer.CLOSE";
- public static final String ACTION_PLAY_PAUSE
- = App.PACKAGE_NAME + ".player.MainPlayer.PLAY_PAUSE";
- static final String ACTION_REPEAT
- = App.PACKAGE_NAME + ".player.MainPlayer.REPEAT";
- static final String ACTION_PLAY_NEXT
- = App.PACKAGE_NAME + ".player.MainPlayer.ACTION_PLAY_NEXT";
- static final String ACTION_PLAY_PREVIOUS
- = App.PACKAGE_NAME + ".player.MainPlayer.ACTION_PLAY_PREVIOUS";
- static final String ACTION_FAST_REWIND
- = App.PACKAGE_NAME + ".player.MainPlayer.ACTION_FAST_REWIND";
- static final String ACTION_FAST_FORWARD
- = App.PACKAGE_NAME + ".player.MainPlayer.ACTION_FAST_FORWARD";
- static final String ACTION_SHUFFLE
- = App.PACKAGE_NAME + ".player.MainPlayer.ACTION_SHUFFLE";
- public static final String ACTION_RECREATE_NOTIFICATION
- = App.PACKAGE_NAME + ".player.MainPlayer.ACTION_RECREATE_NOTIFICATION";
-
/*//////////////////////////////////////////////////////////////////////////
// Service's LifeCycle
//////////////////////////////////////////////////////////////////////////*/
diff --git a/app/src/main/java/org/schabi/newpipe/player/NotificationConstants.java b/app/src/main/java/org/schabi/newpipe/player/notification/NotificationConstants.java
similarity index 82%
rename from app/src/main/java/org/schabi/newpipe/player/NotificationConstants.java
rename to app/src/main/java/org/schabi/newpipe/player/notification/NotificationConstants.java
index 6c9858d1bdf..53ef752bd8a 100644
--- a/app/src/main/java/org/schabi/newpipe/player/NotificationConstants.java
+++ b/app/src/main/java/org/schabi/newpipe/player/notification/NotificationConstants.java
@@ -1,4 +1,4 @@
-package org.schabi.newpipe.player;
+package org.schabi.newpipe.player.notification;
import android.content.Context;
import android.content.SharedPreferences;
@@ -7,6 +7,7 @@
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
+import org.schabi.newpipe.App;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.Localization;
@@ -20,7 +21,34 @@
public final class NotificationConstants {
- private NotificationConstants() { }
+ private NotificationConstants() {
+ }
+
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Intent actions
+ //////////////////////////////////////////////////////////////////////////*/
+
+ public static final String ACTION_CLOSE
+ = App.PACKAGE_NAME + ".player.MainPlayer.CLOSE";
+ public static final String ACTION_PLAY_PAUSE
+ = App.PACKAGE_NAME + ".player.MainPlayer.PLAY_PAUSE";
+ public static final String ACTION_REPEAT
+ = App.PACKAGE_NAME + ".player.MainPlayer.REPEAT";
+ public static final String ACTION_PLAY_NEXT
+ = App.PACKAGE_NAME + ".player.MainPlayer.ACTION_PLAY_NEXT";
+ public static final String ACTION_PLAY_PREVIOUS
+ = App.PACKAGE_NAME + ".player.MainPlayer.ACTION_PLAY_PREVIOUS";
+ public static final String ACTION_FAST_REWIND
+ = App.PACKAGE_NAME + ".player.MainPlayer.ACTION_FAST_REWIND";
+ public static final String ACTION_FAST_FORWARD
+ = App.PACKAGE_NAME + ".player.MainPlayer.ACTION_FAST_FORWARD";
+ public static final String ACTION_SHUFFLE
+ = App.PACKAGE_NAME + ".player.MainPlayer.ACTION_SHUFFLE";
+ public static final String ACTION_RECREATE_NOTIFICATION
+ = App.PACKAGE_NAME + ".player.MainPlayer.ACTION_RECREATE_NOTIFICATION";
+
public static final int NOTHING = 0;
diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/NotificationPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/notification/NotificationPlayerUi.java
similarity index 94%
rename from app/src/main/java/org/schabi/newpipe/player/ui/NotificationPlayerUi.java
rename to app/src/main/java/org/schabi/newpipe/player/notification/NotificationPlayerUi.java
index 5736eca3bbd..ed678a18c09 100644
--- a/app/src/main/java/org/schabi/newpipe/player/ui/NotificationPlayerUi.java
+++ b/app/src/main/java/org/schabi/newpipe/player/notification/NotificationPlayerUi.java
@@ -1,7 +1,7 @@
-package org.schabi.newpipe.player.ui;
+package org.schabi.newpipe.player.notification;
-import static org.schabi.newpipe.player.PlayerService.ACTION_RECREATE_NOTIFICATION;
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE;
+import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_RECREATE_NOTIFICATION;
import android.content.Intent;
import android.graphics.Bitmap;
@@ -12,9 +12,9 @@
import com.google.android.exoplayer2.Player.RepeatMode;
import org.schabi.newpipe.extractor.stream.StreamInfo;
-import org.schabi.newpipe.player.NotificationUtil;
import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.helper.PlayerHelper;
+import org.schabi.newpipe.player.ui.PlayerUi;
public final class NotificationPlayerUi extends PlayerUi {
private boolean foregroundNotificationAlreadyCreated = false;
diff --git a/app/src/main/java/org/schabi/newpipe/player/NotificationUtil.java b/app/src/main/java/org/schabi/newpipe/player/notification/NotificationUtil.java
similarity index 94%
rename from app/src/main/java/org/schabi/newpipe/player/NotificationUtil.java
rename to app/src/main/java/org/schabi/newpipe/player/notification/NotificationUtil.java
index e88defe7f42..5f005245343 100644
--- a/app/src/main/java/org/schabi/newpipe/player/NotificationUtil.java
+++ b/app/src/main/java/org/schabi/newpipe/player/notification/NotificationUtil.java
@@ -1,8 +1,7 @@
-package org.schabi.newpipe.player;
+package org.schabi.newpipe.player.notification;
import android.annotation.SuppressLint;
import android.app.PendingIntent;
-import android.app.Service;
import android.content.Intent;
import android.content.pm.ServiceInfo;
import android.graphics.Bitmap;
@@ -19,6 +18,7 @@
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
+import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.util.NavigationHelper;
import java.util.List;
@@ -26,14 +26,14 @@
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
-import static org.schabi.newpipe.player.PlayerService.ACTION_CLOSE;
-import static org.schabi.newpipe.player.PlayerService.ACTION_FAST_FORWARD;
-import static org.schabi.newpipe.player.PlayerService.ACTION_FAST_REWIND;
-import static org.schabi.newpipe.player.PlayerService.ACTION_PLAY_NEXT;
-import static org.schabi.newpipe.player.PlayerService.ACTION_PLAY_PAUSE;
-import static org.schabi.newpipe.player.PlayerService.ACTION_PLAY_PREVIOUS;
-import static org.schabi.newpipe.player.PlayerService.ACTION_REPEAT;
-import static org.schabi.newpipe.player.PlayerService.ACTION_SHUFFLE;
+import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_CLOSE;
+import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_FAST_FORWARD;
+import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_FAST_REWIND;
+import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_NEXT;
+import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_PAUSE;
+import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_PREVIOUS;
+import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_REPEAT;
+import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_SHUFFLE;
/**
* This is a utility class for player notifications.
@@ -51,7 +51,7 @@ public final class NotificationUtil {
private NotificationManagerCompat notificationManager;
private NotificationCompat.Builder notificationBuilder;
- private Player player;
+ private final Player player;
public NotificationUtil(final Player player) {
this.player = player;
@@ -205,12 +205,11 @@ private void initializeNotificationSlots() {
private void updateActions(final NotificationCompat.Builder builder) {
builder.mActions.clear();
for (int i = 0; i < 5; ++i) {
- addAction(builder, player, notificationSlots[i]);
+ addAction(builder, notificationSlots[i]);
}
}
private void addAction(final NotificationCompat.Builder builder,
- final Player player,
@NotificationConstants.Action final int slot) {
final NotificationCompat.Action action = getAction(slot);
if (action != null) {
diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java
index 7c60671dd75..80230d0f77a 100644
--- a/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java
+++ b/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java
@@ -5,13 +5,13 @@
import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.player.Player.STATE_COMPLETED;
import static org.schabi.newpipe.player.Player.STATE_PAUSED;
-import static org.schabi.newpipe.player.PlayerService.ACTION_PLAY_PAUSE;
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND;
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE;
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_POPUP;
import static org.schabi.newpipe.player.helper.PlayerHelper.getMinimizeOnExitAction;
import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString;
import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientationLocked;
+import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_PAUSE;
import android.app.Activity;
import android.content.Context;
diff --git a/app/src/main/java/org/schabi/newpipe/settings/custom/NotificationActionsPreference.java b/app/src/main/java/org/schabi/newpipe/settings/custom/NotificationActionsPreference.java
index 0eb58f7a9a4..dfcf2e5974e 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/custom/NotificationActionsPreference.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/custom/NotificationActionsPreference.java
@@ -1,5 +1,7 @@
package org.schabi.newpipe.settings.custom;
+import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_RECREATE_NOTIFICATION;
+
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
@@ -27,7 +29,7 @@
import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
import org.schabi.newpipe.player.PlayerService;
-import org.schabi.newpipe.player.NotificationConstants;
+import org.schabi.newpipe.player.notification.NotificationConstants;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.views.FocusOverlayView;
@@ -61,7 +63,7 @@ public void onBindViewHolder(@NonNull final PreferenceViewHolder holder) {
public void onDetached() {
super.onDetached();
saveChanges();
- getContext().sendBroadcast(new Intent(PlayerService.ACTION_RECREATE_NOTIFICATION));
+ getContext().sendBroadcast(new Intent(ACTION_RECREATE_NOTIFICATION));
}
From 8c26403e91de46631fffceeb1216bb20c630033f Mon Sep 17 00:00:00 2001
From: Stypox
Date: Thu, 14 Apr 2022 18:42:22 +0200
Subject: [PATCH 125/240] Remove unused PlayerState
---
.../schabi/newpipe/player/PlayerState.java | 79 -------------------
1 file changed, 79 deletions(-)
delete mode 100644 app/src/main/java/org/schabi/newpipe/player/PlayerState.java
diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayerState.java b/app/src/main/java/org/schabi/newpipe/player/PlayerState.java
deleted file mode 100644
index af875a32ba8..00000000000
--- a/app/src/main/java/org/schabi/newpipe/player/PlayerState.java
+++ /dev/null
@@ -1,79 +0,0 @@
-package org.schabi.newpipe.player;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import org.schabi.newpipe.player.playqueue.PlayQueue;
-
-import java.io.Serializable;
-
-public class PlayerState implements Serializable {
-
- @NonNull
- private final PlayQueue playQueue;
- private final int repeatMode;
- private final float playbackSpeed;
- private final float playbackPitch;
- @Nullable
- private final String playbackQuality;
- private final boolean playbackSkipSilence;
- private final boolean wasPlaying;
-
- PlayerState(@NonNull final PlayQueue playQueue, final int repeatMode,
- final float playbackSpeed, final float playbackPitch,
- final boolean playbackSkipSilence, final boolean wasPlaying) {
- this(playQueue, repeatMode, playbackSpeed, playbackPitch, null,
- playbackSkipSilence, wasPlaying);
- }
-
- PlayerState(@NonNull final PlayQueue playQueue, final int repeatMode,
- final float playbackSpeed, final float playbackPitch,
- @Nullable final String playbackQuality, final boolean playbackSkipSilence,
- final boolean wasPlaying) {
- this.playQueue = playQueue;
- this.repeatMode = repeatMode;
- this.playbackSpeed = playbackSpeed;
- this.playbackPitch = playbackPitch;
- this.playbackQuality = playbackQuality;
- this.playbackSkipSilence = playbackSkipSilence;
- this.wasPlaying = wasPlaying;
- }
-
- /*//////////////////////////////////////////////////////////////////////////
- // Serdes
- //////////////////////////////////////////////////////////////////////////*/
-
- /*//////////////////////////////////////////////////////////////////////////
- // Getters
- //////////////////////////////////////////////////////////////////////////*/
-
- @NonNull
- public PlayQueue getPlayQueue() {
- return playQueue;
- }
-
- public int getRepeatMode() {
- return repeatMode;
- }
-
- public float getPlaybackSpeed() {
- return playbackSpeed;
- }
-
- public float getPlaybackPitch() {
- return playbackPitch;
- }
-
- @Nullable
- public String getPlaybackQuality() {
- return playbackQuality;
- }
-
- public boolean isPlaybackSkipSilence() {
- return playbackSkipSilence;
- }
-
- public boolean wasPlaying() {
- return wasPlaying;
- }
-}
From 6fb02569978c8126cb24376419980a8321843b33 Mon Sep 17 00:00:00 2001
From: Stypox
Date: Thu, 14 Apr 2022 18:43:54 +0200
Subject: [PATCH 126/240] Remove unused PlayerServiceBinder
---
.../newpipe/player/PlayQueueActivity.java | 4 +---
.../newpipe/player/PlayerServiceBinder.java | 17 -----------------
2 files changed, 1 insertion(+), 20 deletions(-)
delete mode 100644 app/src/main/java/org/schabi/newpipe/player/PlayerServiceBinder.java
diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java b/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java
index d00e6265e0c..cdba900f9bf 100644
--- a/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java
@@ -207,9 +207,7 @@ public void onServiceDisconnected(final ComponentName name) {
public void onServiceConnected(final ComponentName name, final IBinder service) {
Log.d(TAG, "Player service is connected");
- if (service instanceof PlayerServiceBinder) {
- player = ((PlayerServiceBinder) service).getPlayerInstance();
- } else if (service instanceof PlayerService.LocalBinder) {
+ if (service instanceof PlayerService.LocalBinder) {
player = ((PlayerService.LocalBinder) service).getPlayer();
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayerServiceBinder.java b/app/src/main/java/org/schabi/newpipe/player/PlayerServiceBinder.java
deleted file mode 100644
index 5c28c6c7b1b..00000000000
--- a/app/src/main/java/org/schabi/newpipe/player/PlayerServiceBinder.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package org.schabi.newpipe.player;
-
-import android.os.Binder;
-
-import androidx.annotation.NonNull;
-
-class PlayerServiceBinder extends Binder {
- private final Player player;
-
- PlayerServiceBinder(@NonNull final Player player) {
- this.player = player;
- }
-
- Player getPlayerInstance() {
- return player;
- }
-}
From fa25ecf52143a3cb8b695db7c25799de320b1e5f Mon Sep 17 00:00:00 2001
From: Stypox
Date: Thu, 14 Apr 2022 18:55:23 +0200
Subject: [PATCH 127/240] Add comment about broadcast receiver
---
app/src/main/java/org/schabi/newpipe/player/Player.java | 6 ++++++
.../main/java/org/schabi/newpipe/player/ui/PlayerUi.java | 4 ++++
2 files changed, 10 insertions(+)
diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java
index 55600b95698..b0fed3d7d70 100644
--- a/app/src/main/java/org/schabi/newpipe/player/Player.java
+++ b/app/src/main/java/org/schabi/newpipe/player/Player.java
@@ -663,6 +663,12 @@ public void smoothStopForImmediateReusing() {
//////////////////////////////////////////////////////////////////////////*/
//region Broadcast receiver
+ /**
+ * This function prepares the broadcast receiver and is called only in the constructor.
+ * Therefore if you want any PlayerUi to receive a broadcast action, you should add it here,
+ * even if that player ui might never be added to the player. In that case the received
+ * broadcast would not do anything.
+ */
private void setupBroadcastReceiver() {
if (DEBUG) {
Log.d(TAG, "setupBroadcastReceiver() called");
diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java
index 15b468fb715..81e93ca238f 100644
--- a/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java
+++ b/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java
@@ -55,6 +55,10 @@ public void smoothStopForImmediateReusing() {
public void onFragmentListenerSet() {
}
+ /**
+ * If you want to register new broadcast actions to receive here, add them to
+ * {@link Player#setupBroadcastReceiver()}.
+ */
public void onBroadcastReceived(final Intent intent) {
}
From 6559416bd8e0857a32b00c097638be9dc2eec88b Mon Sep 17 00:00:00 2001
From: Stypox
Date: Thu, 14 Apr 2022 23:07:29 +0200
Subject: [PATCH 128/240] Improve //region comments in player UIs
---
.../newpipe/player/ui/MainPlayerUi.java | 71 ++++++----
.../newpipe/player/ui/PopupPlayerUi.java | 80 +++++++----
.../newpipe/player/ui/VideoPlayerUi.java | 131 ++++++++++--------
3 files changed, 172 insertions(+), 110 deletions(-)
diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java
index 80230d0f77a..c62382782e3 100644
--- a/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java
+++ b/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java
@@ -88,6 +88,12 @@ public final class MainPlayerUi extends VideoPlayerUi implements View.OnLayoutCh
// fullscreen player
private ItemTouchHelper itemTouchHelper;
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Constructor, setup, destroy
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Constructor, setup, destroy
+
public MainPlayerUi(@NonNull final Player player,
@NonNull final PlayerBinding playerBinding) {
super(player, playerBinding);
@@ -272,12 +278,14 @@ protected void setupElementsSize(final Resources resources) {
resources.getDimensionPixelSize(R.dimen.player_main_buttons_padding)
);
}
+ //endregion
/*//////////////////////////////////////////////////////////////////////////
// Broadcast receiver
//////////////////////////////////////////////////////////////////////////*/
//region Broadcast receiver
+
@Override
public void onBroadcastReceived(final Intent intent) {
super.onBroadcastReceived(intent);
@@ -313,6 +321,7 @@ public void onBroadcastReceived(final Intent intent) {
// Fragment binding
//////////////////////////////////////////////////////////////////////////*/
//region Fragment binding
+
@Override
public void onFragmentListenerSet() {
super.onFragmentListenerSet();
@@ -351,13 +360,11 @@ private void onFragmentStopped() {
}
//endregion
- private void showHideKodiButton() {
- // show kodi button if it supports the current service and it is enabled in settings
- @Nullable final PlayQueue playQueue = player.getPlayQueue();
- binding.playWithKodi.setVisibility(playQueue != null && playQueue.getItem() != null
- && KoreUtils.shouldShowPlayWithKodi(context, playQueue.getItem().getServiceId())
- ? View.VISIBLE : View.GONE);
- }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Playback states
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Playback states
@Override
public void onUpdateProgress(final int currentProgress,
@@ -373,6 +380,22 @@ public void onUpdateProgress(final int currentProgress,
}
}
+ @Override
+ public void onPlaying() {
+ super.onPlaying();
+ checkLandscape();
+ }
+
+ @Override
+ public void onCompleted() {
+ super.onCompleted();
+ if (isFullscreen) {
+ toggleFullscreen();
+ }
+ }
+ //endregion
+
+
/*//////////////////////////////////////////////////////////////////////////
// Controls showing / hiding
//////////////////////////////////////////////////////////////////////////*/
@@ -457,22 +480,21 @@ protected float calculateMaxEndScreenThumbnailHeight(@NonNull final Bitmap bitma
return Math.min(bitmap.getHeight(), screenHeight);
}
}
- //endregion
- @Override
- public void onPlaying() {
- super.onPlaying();
- checkLandscape();
+ private void showHideKodiButton() {
+ // show kodi button if it supports the current service and it is enabled in settings
+ @Nullable final PlayQueue playQueue = player.getPlayQueue();
+ binding.playWithKodi.setVisibility(playQueue != null && playQueue.getItem() != null
+ && KoreUtils.shouldShowPlayWithKodi(context, playQueue.getItem().getServiceId())
+ ? View.VISIBLE : View.GONE);
}
+ //endregion
- @Override
- public void onCompleted() {
- super.onCompleted();
- if (isFullscreen) {
- toggleFullscreen();
- }
- }
+ /*//////////////////////////////////////////////////////////////////////////
+ // Captions (text tracks)
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Captions (text tracks)
@Override
protected void setupSubtitleView(float captionScale) {
@@ -482,8 +504,7 @@ protected void setupSubtitleView(float captionScale) {
binding.subtitleView.setFixedTextSize(
TypedValue.COMPLEX_UNIT_PX, minimumLength / captionRatioInverse);
}
-
-
+ //endregion
/*//////////////////////////////////////////////////////////////////////////
@@ -798,6 +819,7 @@ public boolean isVerticalVideo() {
// Click listeners
//////////////////////////////////////////////////////////////////////////*/
//region Click listeners
+
@Override
public void onClick(final View v) {
if (v.getId() == binding.screenRotationButton.getId()) {
@@ -855,9 +877,9 @@ public boolean onKeyDown(final int keyCode) {
/*//////////////////////////////////////////////////////////////////////////
- // Video size, resize, orientation, fullscreen
+ // Video size, orientation, fullscreen
//////////////////////////////////////////////////////////////////////////*/
- //region Video size, resize, orientation, fullscreen
+ //region Video size, orientation, fullscreen
private void setupScreenRotationButton() {
binding.screenRotationButton.setVisibility(globalScreenOrientationLocked(context)
@@ -941,9 +963,6 @@ public void checkLandscape() {
// Getters
//////////////////////////////////////////////////////////////////////////*/
//region Getters
- public PlayerBinding getBinding() {
- return binding;
- }
public Optional getParentActivity() {
final ViewParent rootParent = binding.getRoot().getParent();
diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/PopupPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/PopupPlayerUi.java
index 7df9102b75d..43440b87359 100644
--- a/app/src/main/java/org/schabi/newpipe/player/ui/PopupPlayerUi.java
+++ b/app/src/main/java/org/schabi/newpipe/player/ui/PopupPlayerUi.java
@@ -8,7 +8,6 @@
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.annotation.SuppressLint;
-import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Bitmap;
@@ -61,6 +60,12 @@ public final class PopupPlayerUi extends VideoPlayerUi {
private WindowManager.LayoutParams popupLayoutParams; // null if player is not popup
private final WindowManager windowManager;
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Constructor, setup, destroy
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Constructor, setup, destroy
+
public PopupPlayerUi(@NonNull final Player player,
@NonNull final PlayerBinding playerBinding) {
super(player, playerBinding);
@@ -173,11 +178,14 @@ public void destroy() {
super.destroy();
removePopupFromView();
}
+ //endregion
+
/*//////////////////////////////////////////////////////////////////////////
// Broadcast receiver
//////////////////////////////////////////////////////////////////////////*/
//region Broadcast receiver
+
@Override
public void onBroadcastReceived(final Intent intent) {
super.onBroadcastReceived(intent);
@@ -200,6 +208,11 @@ public void onBroadcastReceived(final Intent intent) {
//endregion
+ /*//////////////////////////////////////////////////////////////////////////
+ // Popup position and size
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Popup position and size
+
/**
* Check if {@link #popupLayoutParams}' position is within a arbitrary boundary
* that goes from (0, 0) to (screenWidth, screenHeight).
@@ -272,16 +285,19 @@ public void changePopupSize(final int width) {
windowManager.updateViewLayout(binding.getRoot(), popupLayoutParams);
}
- private void changePopupWindowFlags(final int flags) {
- if (DEBUG) {
- Log.d(TAG, "changePopupWindowFlags() called with: flags = [" + flags + "]");
- }
-
- if (!anyPopupViewIsNull()) {
- popupLayoutParams.flags = flags;
- windowManager.updateViewLayout(binding.getRoot(), popupLayoutParams);
- }
+ @Override
+ protected float calculateMaxEndScreenThumbnailHeight(@NonNull final Bitmap bitmap) {
+ // no need for the end screen thumbnail to be resized on popup player: it's only needed
+ // for the main player so that it is enlarged correctly inside the fragment
+ return bitmap.getHeight();
}
+ //endregion
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Popup closing
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Popup closing
public void closePopup() {
if (DEBUG) {
@@ -351,23 +367,22 @@ private void end() {
}
}).start();
}
+ //endregion
- @Override
- protected float calculateMaxEndScreenThumbnailHeight(@NonNull final Bitmap bitmap) {
- // no need for the end screen thumbnail to be resized on popup player: it's only needed
- // for the main player so that it is enlarged correctly inside the fragment
- return bitmap.getHeight();
- }
+ /*//////////////////////////////////////////////////////////////////////////
+ // Playback states
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Playback states
- private boolean popupHasParent() {
- return binding != null
- && binding.getRoot().getLayoutParams() instanceof WindowManager.LayoutParams
- && binding.getRoot().getParent() != null;
- }
+ private void changePopupWindowFlags(final int flags) {
+ if (DEBUG) {
+ Log.d(TAG, "changePopupWindowFlags() called with: flags = [" + flags + "]");
+ }
- private boolean anyPopupViewIsNull() {
- return popupLayoutParams == null || windowManager == null
- || binding.getRoot().getParent() == null;
+ if (!anyPopupViewIsNull()) {
+ popupLayoutParams.flags = flags;
+ windowManager.updateViewLayout(binding.getRoot(), popupLayoutParams);
+ }
}
@Override
@@ -400,11 +415,14 @@ protected void onPlaybackSpeedClicked() {
playbackSpeedPopupMenu.show();
isSomePopupMenuVisible = true;
}
+ //endregion
+
/*//////////////////////////////////////////////////////////////////////////
// Gestures
//////////////////////////////////////////////////////////////////////////*/
//region Gestures
+
private int distanceFromCloseButton(@NonNull final MotionEvent popupMotionEvent) {
final int closeOverlayButtonX = closeOverlayBinding.closeButton.getLeft()
+ closeOverlayBinding.closeButton.getWidth() / 2;
@@ -433,7 +451,19 @@ public boolean isInsideClosingRadius(@NonNull final MotionEvent popupMotionEvent
/*//////////////////////////////////////////////////////////////////////////
// Getters
//////////////////////////////////////////////////////////////////////////*/
- //region Gestures
+ //region Getters
+
+ private boolean popupHasParent() {
+ return binding != null
+ && binding.getRoot().getLayoutParams() instanceof WindowManager.LayoutParams
+ && binding.getRoot().getParent() != null;
+ }
+
+ private boolean anyPopupViewIsNull() {
+ return popupLayoutParams == null || windowManager == null
+ || binding.getRoot().getParent() == null;
+ }
+
public PlayerPopupCloseOverlayBinding getCloseOverlayBinding() {
return closeOverlayBinding;
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java
index 24cdb8908bb..f4ebc3304b5 100644
--- a/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java
+++ b/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java
@@ -135,6 +135,12 @@ public abstract class VideoPlayerUi extends PlayerUi
@NonNull private final SeekbarPreviewThumbnailHolder seekbarPreviewThumbnailHolder =
new SeekbarPreviewThumbnailHolder();
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Constructor, setup, destroy
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Constructor, setup, destroy
+
public VideoPlayerUi(@NonNull final Player player,
@NonNull final PlayerBinding playerBinding) {
super(player);
@@ -142,11 +148,6 @@ public VideoPlayerUi(@NonNull final Player player,
setupFromView();
}
-
- /*//////////////////////////////////////////////////////////////////////////
- // Setup
- //////////////////////////////////////////////////////////////////////////*/
- //region Setup
public void setupFromView() {
initViews();
initListeners();
@@ -414,6 +415,7 @@ protected void setupElementsSize(final int buttonsMinWidth,
// Broadcast receiver
//////////////////////////////////////////////////////////////////////////*/
//region Broadcast receiver
+
@Override
public void onBroadcastReceived(final Intent intent) {
super.onBroadcastReceived(intent);
@@ -433,6 +435,7 @@ public void onBroadcastReceived(final Intent intent) {
// Thumbnail
//////////////////////////////////////////////////////////////////////////*/
//region Thumbnail
+
/**
* Scale the player audio / end screen thumbnail down if necessary.
*
@@ -481,6 +484,7 @@ private void updateEndScreenThumbnail() {
// Progress loop and updates
//////////////////////////////////////////////////////////////////////////*/
//region Progress loop and updates
+
@Override
public void onUpdateProgress(final int currentProgress,
final int duration,
@@ -744,6 +748,7 @@ public boolean isFullscreen() {
// Playback states
//////////////////////////////////////////////////////////////////////////*/
//region Playback states
+
@Override
public void onPrepared() {
super.onPrepared();
@@ -885,7 +890,8 @@ private void animatePlayButtons(final boolean show, final long duration) {
/*//////////////////////////////////////////////////////////////////////////
// Repeat, shuffle, mute
//////////////////////////////////////////////////////////////////////////*/
- //region Repeat and shuffle
+ //region Repeat, shuffle, mute
+
public void onRepeatClicked() {
if (DEBUG) {
Log.d(TAG, "onRepeatClicked() called");
@@ -945,52 +951,9 @@ private void setShuffleButton(final boolean shuffled) {
/*//////////////////////////////////////////////////////////////////////////
- // ExoPlayer listeners (that didn't fit in other categories)
+ // Other player listeners
//////////////////////////////////////////////////////////////////////////*/
- //region ExoPlayer listeners (that didn't fit in other categories)
- @Override
- public void onTextTracksChanged(@NonNull final Tracks currentTracks) {
- super.onTextTracksChanged(currentTracks);
-
- final boolean trackTypeTextSupported = !currentTracks.containsType(C.TRACK_TYPE_TEXT)
- || currentTracks.isTypeSupported(C.TRACK_TYPE_TEXT, false);
- if (getPlayer().getTrackSelector().getCurrentMappedTrackInfo() == null
- || !trackTypeTextSupported) {
- binding.captionTextView.setVisibility(View.GONE);
- return;
- }
-
- // Extract all loaded languages
- final List textTracks = currentTracks
- .getGroups()
- .stream()
- .filter(trackGroupInfo -> C.TRACK_TYPE_TEXT == trackGroupInfo.getType())
- .collect(Collectors.toList());
- final List availableLanguages = textTracks.stream()
- .map(Tracks.Group::getMediaTrackGroup)
- .filter(textTrack -> textTrack.length > 0)
- .map(textTrack -> textTrack.getFormat(0).language)
- .collect(Collectors.toList());
-
- // Find selected text track
- final Optional selectedTracks = textTracks.stream()
- .filter(Tracks.Group::isSelected)
- .filter(info -> info.getMediaTrackGroup().length >= 1)
- .map(info -> info.getMediaTrackGroup().getFormat(0))
- .findFirst();
-
- // Build UI
- buildCaptionMenu(availableLanguages);
- //noinspection SimplifyOptionalCallChains
- if (player.getTrackSelector().getParameters().getRendererDisabled(
- player.getCaptionRendererIndex()) || !selectedTracks.isPresent()) {
- binding.captionTextView.setText(R.string.caption_none);
- } else {
- binding.captionTextView.setText(selectedTracks.get().language);
- }
- binding.captionTextView.setVisibility(
- availableLanguages.isEmpty() ? View.GONE : View.VISIBLE);
- }
+ //region Other player listeners
@Override
public void onPlaybackParametersChanged(@NonNull final PlaybackParameters playbackParameters) {
@@ -1004,12 +967,6 @@ public void onRenderedFirstFrame() {
//TODO check if this causes black screen when switching to fullscreen
animate(binding.surfaceForeground, false, DEFAULT_CONTROLS_DURATION);
}
-
- @Override
- public void onCues(@NonNull List cues) {
- super.onCues(cues);
- binding.subtitleView.setCues(cues);
- }
//endregion
@@ -1017,6 +974,7 @@ public void onCues(@NonNull List cues) {
// Metadata & stream related views
//////////////////////////////////////////////////////////////////////////*/
//region Metadata & stream related views
+
@Override
public void onMetadataChanged(@NonNull final StreamInfo info) {
super.onMetadataChanged(info);
@@ -1092,6 +1050,7 @@ private void updateStreamRelatedViews() {
// Popup menus ("popup" means that they pop up, not that they belong to the popup player)
//////////////////////////////////////////////////////////////////////////*/
//region Popup menus ("popup" means that they pop up, not that they belong to the popup player)
+
private void buildQualityMenu() {
if (qualityPopupMenu == null) {
return;
@@ -1315,6 +1274,57 @@ public boolean isSomePopupMenuVisible() {
// Captions (text tracks)
//////////////////////////////////////////////////////////////////////////*/
//region Captions (text tracks)
+
+ @Override
+ public void onTextTracksChanged(@NonNull final Tracks currentTracks) {
+ super.onTextTracksChanged(currentTracks);
+
+ final boolean trackTypeTextSupported = !currentTracks.containsType(C.TRACK_TYPE_TEXT)
+ || currentTracks.isTypeSupported(C.TRACK_TYPE_TEXT, false);
+ if (getPlayer().getTrackSelector().getCurrentMappedTrackInfo() == null
+ || !trackTypeTextSupported) {
+ binding.captionTextView.setVisibility(View.GONE);
+ return;
+ }
+
+ // Extract all loaded languages
+ final List textTracks = currentTracks
+ .getGroups()
+ .stream()
+ .filter(trackGroupInfo -> C.TRACK_TYPE_TEXT == trackGroupInfo.getType())
+ .collect(Collectors.toList());
+ final List availableLanguages = textTracks.stream()
+ .map(Tracks.Group::getMediaTrackGroup)
+ .filter(textTrack -> textTrack.length > 0)
+ .map(textTrack -> textTrack.getFormat(0).language)
+ .collect(Collectors.toList());
+
+ // Find selected text track
+ final Optional selectedTracks = textTracks.stream()
+ .filter(Tracks.Group::isSelected)
+ .filter(info -> info.getMediaTrackGroup().length >= 1)
+ .map(info -> info.getMediaTrackGroup().getFormat(0))
+ .findFirst();
+
+ // Build UI
+ buildCaptionMenu(availableLanguages);
+ //noinspection SimplifyOptionalCallChains
+ if (player.getTrackSelector().getParameters().getRendererDisabled(
+ player.getCaptionRendererIndex()) || !selectedTracks.isPresent()) {
+ binding.captionTextView.setText(R.string.caption_none);
+ } else {
+ binding.captionTextView.setText(selectedTracks.get().language);
+ }
+ binding.captionTextView.setVisibility(
+ availableLanguages.isEmpty() ? View.GONE : View.VISIBLE);
+ }
+
+ @Override
+ public void onCues(@NonNull List cues) {
+ super.onCues(cues);
+ binding.subtitleView.setCues(cues);
+ }
+
private void setupSubtitleView() {
setupSubtitleView(PlayerHelper.getCaptionScale(context));
final CaptionStyleCompat captionStyle = PlayerHelper.getCaptionStyle(context);
@@ -1330,6 +1340,7 @@ private void setupSubtitleView() {
// Click listeners
//////////////////////////////////////////////////////////////////////////*/
//region Click listeners
+
@Override
public void onClick(final View v) {
if (DEBUG) {
@@ -1493,9 +1504,10 @@ private void onOpenInBrowserClicked() {
/*//////////////////////////////////////////////////////////////////////////
- // Video size, resize, orientation, fullscreen
+ // Video size
//////////////////////////////////////////////////////////////////////////*/
- //region Video size, resize, orientation, fullscreen
+ //region Video size
+
protected void setResizeMode(@AspectRatioFrameLayout.ResizeMode final int resizeMode) {
binding.surfaceView.setResizeMode(resizeMode);
binding.resizeTextView.setText(PlayerHelper.resizeTypeOf(context, resizeMode));
@@ -1569,6 +1581,7 @@ private void clearVideoSurface() {
// Getters
//////////////////////////////////////////////////////////////////////////*/
//region Getters
+
public PlayerBinding getBinding() {
return binding;
}
From 1b39b5376f518ee570258da82fbe4fb09d11c231 Mon Sep 17 00:00:00 2001
From: Stypox
Date: Fri, 15 Apr 2022 00:01:59 +0200
Subject: [PATCH 129/240] Add some javadocs; move preparing player uis to
PlayerUiList
---
.../org/schabi/newpipe/player/Player.java | 25 ++---
.../schabi/newpipe/player/ui/PlayerUi.java | 92 ++++++++++++++++++-
.../newpipe/player/ui/PlayerUiList.java | 43 ++++++++-
3 files changed, 138 insertions(+), 22 deletions(-)
diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java
index b0fed3d7d70..f8ea7bc90cc 100644
--- a/app/src/main/java/org/schabi/newpipe/player/Player.java
+++ b/app/src/main/java/org/schabi/newpipe/player/Player.java
@@ -447,7 +447,7 @@ && isPlaybackResumeEnabled(this)
private void initUIsForCurrentPlayerType() {
//noinspection SimplifyOptionalCallChains
if (!UIs.get(NotificationPlayerUi.class).isPresent()) {
- UIs.add(new NotificationPlayerUi(this));
+ UIs.addAndPrepare(new NotificationPlayerUi(this));
}
if ((UIs.get(MainPlayerUi.class).isPresent() && playerType == PlayerType.MAIN)
@@ -469,24 +469,15 @@ private void initUIsForCurrentPlayerType() {
switch (playerType) {
case MAIN:
UIs.destroyAll(PopupPlayerUi.class);
- UIs.add(new MainPlayerUi(this, binding));
- break;
- case AUDIO:
- UIs.destroyAll(VideoPlayerUi.class);
+ UIs.addAndPrepare(new MainPlayerUi(this, binding));
break;
case POPUP:
UIs.destroyAll(MainPlayerUi.class);
- UIs.add(new PopupPlayerUi(this, binding));
+ UIs.addAndPrepare(new PopupPlayerUi(this, binding));
+ break;
+ case AUDIO:
+ UIs.destroyAll(VideoPlayerUi.class);
break;
- }
-
- if (fragmentListener != null) {
- // make sure UIs know whether a service is connected or not
- UIs.call(PlayerUi::onFragmentListenerSet);
- }
- if (!exoPlayerIsNull()) {
- UIs.call(PlayerUi::initPlayer);
- UIs.call(PlayerUi::initPlayback);
}
}
@@ -1968,9 +1959,9 @@ public int getCaptionRendererIndex() {
/*//////////////////////////////////////////////////////////////////////////
- // Video size, resize, orientation, fullscreen
+ // Video size
//////////////////////////////////////////////////////////////////////////*/
- //region Video size, resize, orientation, fullscreen
+ //region Video size
@Override // exoplayer listener
public void onVideoSizeChanged(@NonNull final VideoSize videoSize) {
if (DEBUG) {
diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java
index 81e93ca238f..c4db1f33426 100644
--- a/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java
+++ b/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java
@@ -18,50 +18,105 @@
import java.util.List;
+/**
+ * A player UI is a component that can seamlessly connect and disconnect from the {@link Player} and
+ * provide a user interface of some sort. Try to extend this class instead of adding more code to
+ * {@link Player}!
+ */
public abstract class PlayerUi {
- @NonNull protected Context context;
- @NonNull protected Player player;
+ @NonNull protected final Context context;
+ @NonNull protected final Player player;
+ /**
+ * @param player the player instance that will be usable throughout the lifetime of this UI
+ */
public PlayerUi(@NonNull final Player player) {
this.context = player.getContext();
this.player = player;
}
+ /**
+ * @return the player instance this UI was constructed with
+ */
@NonNull
public Player getPlayer() {
return player;
}
+ /**
+ * Called after the player received an intent and processed it
+ */
public void setupAfterIntent() {
}
+ /**
+ * Called right after the exoplayer instance is constructed, or right after this UI is
+ * constructed if the exoplayer is already available then. Note that the exoplayer instance
+ * could be built and destroyed multiple times during the lifetime of the player, so this method
+ * might be called multiple times.
+ */
public void initPlayer() {
}
+ /**
+ * Called when playback in the exoplayer is about to start, or right after this UI is
+ * constructed if the exoplayer and the play queue are already available then. The play queue
+ * will therefore always be not null.
+ */
public void initPlayback() {
}
+ /**
+ * Called when the exoplayer instance is about to be destroyed. Note that the exoplayer instance
+ * could be built and destroyed multiple times during the lifetime of the player, so this method
+ * might be called multiple times. Be sure to unset any video surface view or play queue
+ * listeners! This will also be called when this UI is being discarded, just before {@link
+ * #destroy()}.
+ */
public void destroyPlayer() {
}
+ /**
+ * Called when this UI is being discarded, either because the player is switching to a different
+ * UI or because the player is shutting down completely
+ */
public void destroy() {
}
+ /**
+ * Called when the player is smooth-stopping, that is, transitioning smoothly to a new play
+ * queue after the user tapped on a new video stream while a stream was playing in the video
+ * detail fragment
+ */
public void smoothStopForImmediateReusing() {
}
+ /**
+ * Called when the video detail fragment listener is connected with the player, or right after
+ * this UI is constructed if the listener is already connected then
+ */
public void onFragmentListenerSet() {
}
/**
- * If you want to register new broadcast actions to receive here, add them to
- * {@link Player#setupBroadcastReceiver()}.
+ * Broadcasts that the player receives will also be notified to UIs here. If you want to
+ * register new broadcast actions to receive here, add them to {@link
+ * Player#setupBroadcastReceiver()}.
*/
public void onBroadcastReceived(final Intent intent) {
}
+ /**
+ * Called when stream progress (i.e. the current time in the seekbar) or stream duration change.
+ * Will surely be called every {@link Player#PROGRESS_LOOP_INTERVAL_MILLIS} while a stream is
+ * playing.
+ * @param currentProgress the current progress in milliseconds
+ * @param duration the duration of the stream being played
+ * @param bufferPercent the percentage of stream already buffered, see {@link
+ * com.google.android.exoplayer2.BasePlayer#getBufferedPercentage()}
+ */
public void onUpdateProgress(final int currentProgress,
final int duration,
final int bufferPercent) {
@@ -97,27 +152,56 @@ public void onShuffleModeEnabledChanged(final boolean shuffleModeEnabled) {
public void onMuteUnmuteChanged(final boolean isMuted) {
}
+ /**
+ * @see com.google.android.exoplayer2.Player.Listener#onTracksChanged(Tracks)
+ */
public void onTextTracksChanged(@NonNull final Tracks currentTracks) {
}
+ /**
+ * @see com.google.android.exoplayer2.Player.Listener#onPlaybackParametersChanged
+ */
public void onPlaybackParametersChanged(@NonNull final PlaybackParameters playbackParameters) {
}
+ /**
+ * @see com.google.android.exoplayer2.Player.Listener#onRenderedFirstFrame
+ */
public void onRenderedFirstFrame() {
}
+ /**
+ * @see com.google.android.exoplayer2.text.TextOutput#onCues
+ */
public void onCues(@NonNull final List cues) {
}
+ /**
+ * Called when the stream being played changes
+ * @param info the {@link StreamInfo} metadata object, along with data about the selected and
+ * available video streams (to be used to build the resolution menus, for example)
+ */
public void onMetadataChanged(@NonNull final StreamInfo info) {
}
+ /**
+ * Called when the thumbnail for the current metadata was loaded
+ * @param bitmap the thumbnail to process, or null if there is no thumbnail or there was an
+ * error when loading the thumbnail
+ */
public void onThumbnailLoaded(@Nullable final Bitmap bitmap) {
}
+ /**
+ * Called when the play queue was edited: a stream was appended, moved or removed.
+ */
public void onPlayQueueEdited() {
}
+ /**
+ * @param videoSize the new video size, useful to set the surface aspect ratio
+ * @see com.google.android.exoplayer2.Player.Listener#onVideoSizeChanged
+ */
public void onVideoSizeChanged(@NonNull final VideoSize videoSize) {
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUiList.java b/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUiList.java
index 8c5c0dbfabd..749cda02c61 100644
--- a/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUiList.java
+++ b/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUiList.java
@@ -8,10 +8,39 @@
public final class PlayerUiList {
final List playerUis = new ArrayList<>();
- public void add(final PlayerUi playerUi) {
+ /**
+ * Adds the provided player ui to the list and calls on it the initialization functions that
+ * apply based on the current player state. The preparation step needs to be done since when UIs
+ * are removed and re-added, the player will not call e.g. initPlayer again since the exoplayer
+ * is already initialized, but we need to notify the newly built UI that the player is ready
+ * nonetheless.
+ * @param playerUi the player ui to prepare and add to the list; its {@link
+ * PlayerUi#getPlayer()} will be used to query information about the player
+ * state
+ */
+ public void addAndPrepare(final PlayerUi playerUi) {
+ if (playerUi.getPlayer().getFragmentListener().isPresent()) {
+ // make sure UIs know whether a service is connected or not
+ playerUi.onFragmentListenerSet();
+ }
+
+ if (!playerUi.getPlayer().exoPlayerIsNull()) {
+ playerUi.initPlayer();
+ if (playerUi.getPlayer().getPlayQueue() != null) {
+ playerUi.initPlayback();
+ }
+ }
+
playerUis.add(playerUi);
}
+ /**
+ * Destroys all matching player UIs and removes them from the list
+ * @param playerUiType the class of the player UI to destroy; the {@link
+ * Class#isInstance(Object)} method will be used, so even subclasses will be
+ * destroyed and removed
+ * @param the class type parameter
+ */
public void destroyAll(final Class playerUiType) {
playerUis.stream()
.filter(playerUiType::isInstance)
@@ -22,6 +51,14 @@ public void destroyAll(final Class playerUiType) {
playerUis.removeIf(playerUiType::isInstance);
}
+ /**
+ * @param playerUiType the class of the player UI to return; the {@link
+ * Class#isInstance(Object)} method will be used, so even subclasses could
+ * be returned
+ * @param the class type parameter
+ * @return the first player UI of the required type found in the list, or an empty {@link
+ * Optional} otherwise
+ */
public Optional get(final Class playerUiType) {
return playerUis.stream()
.filter(playerUiType::isInstance)
@@ -29,6 +66,10 @@ public Optional get(final Class playerUiType) {
.findFirst();
}
+ /**
+ * Calls the provided consumer on all player UIs in the list
+ * @param consumer the consumer to call with player UIs
+ */
public void call(final Consumer consumer) {
//noinspection SimplifyStreamApiCallChains
playerUis.stream().forEach(consumer);
From a19073ec011e7c314ccab2e9d84d466d235fd24a Mon Sep 17 00:00:00 2001
From: Stypox
Date: Sat, 16 Apr 2022 12:03:59 +0200
Subject: [PATCH 130/240] Restore checkstyle and solve its errors
---
app/build.gradle | 2 +-
.../fragments/detail/VideoDetailFragment.java | 25 +++++++++----------
.../org/schabi/newpipe/player/Player.java | 6 +++--
.../player/notification/NotificationUtil.java | 4 +--
.../player/playback/PlayerMediaSession.java | 2 --
.../newpipe/player/ui/MainPlayerUi.java | 2 +-
.../schabi/newpipe/player/ui/PlayerUi.java | 16 +++++++-----
.../newpipe/player/ui/PlayerUiList.java | 4 +--
.../newpipe/player/ui/VideoPlayerUi.java | 15 ++++++-----
.../custom/NotificationActionsPreference.java | 1 -
10 files changed, 41 insertions(+), 36 deletions(-)
diff --git a/app/build.gradle b/app/build.gradle
index 46eee8d00c6..9867037e6a9 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -166,7 +166,7 @@ afterEvaluate {
if (!System.properties.containsKey('skipFormatKtlint')) {
preDebugBuild.dependsOn formatKtlint
}
- //preDebugBuild.dependsOn runCheckstyle, runKtlint
+ preDebugBuild.dependsOn runCheckstyle, runKtlint
}
sonarqube {
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java
index cb8f0961f5d..8ffff2f9ef1 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java
@@ -1,5 +1,16 @@
package org.schabi.newpipe.fragments.detail;
+import static android.text.TextUtils.isEmpty;
+import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.COMMENTS;
+import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
+import static org.schabi.newpipe.ktx.ViewUtils.animate;
+import static org.schabi.newpipe.ktx.ViewUtils.animateRotation;
+import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientationLocked;
+import static org.schabi.newpipe.player.helper.PlayerHelper.isClearingQueueConfirmationRequired;
+import static org.schabi.newpipe.player.playqueue.PlayQueueItem.RECOVERY_UNSET;
+import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
+import static org.schabi.newpipe.util.ListHelper.getUrlAndNonTorrentStreams;
+
import android.animation.ValueAnimator;
import android.app.Activity;
import android.content.BroadcastReceiver;
@@ -43,7 +54,6 @@
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.core.content.ContextCompat;
import androidx.preference.PreferenceManager;
-import androidx.viewbinding.ViewBinding;
import com.google.android.exoplayer2.PlaybackException;
import com.google.android.exoplayer2.PlaybackParameters;
@@ -78,9 +88,9 @@
import org.schabi.newpipe.ktx.AnimationType;
import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.local.history.HistoryRecordManager;
+import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.PlayerService;
import org.schabi.newpipe.player.PlayerService.PlayerType;
-import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.event.OnKeyDownListener;
import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener;
import org.schabi.newpipe.player.helper.PlayerHelper;
@@ -118,17 +128,6 @@
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
-import static android.text.TextUtils.isEmpty;
-import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.COMMENTS;
-import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
-import static org.schabi.newpipe.ktx.ViewUtils.animate;
-import static org.schabi.newpipe.ktx.ViewUtils.animateRotation;
-import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientationLocked;
-import static org.schabi.newpipe.player.helper.PlayerHelper.isClearingQueueConfirmationRequired;
-import static org.schabi.newpipe.player.playqueue.PlayQueueItem.RECOVERY_UNSET;
-import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
-import static org.schabi.newpipe.util.ListHelper.getUrlAndNonTorrentStreams;
-
public final class VideoDetailFragment
extends BaseStateFragment
implements BackPressable,
diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java
index f8ea7bc90cc..0755f9b4de0 100644
--- a/app/src/main/java/org/schabi/newpipe/player/Player.java
+++ b/app/src/main/java/org/schabi/newpipe/player/Player.java
@@ -239,6 +239,7 @@ public final class Player implements PlaybackListener, Listener {
// UIs, listeners and disposables
//////////////////////////////////////////////////////////////////////////*/
+ @SuppressWarnings("MemberName") // keep the unusual member name
private final PlayerUiList UIs = new PlayerUiList();
private BroadcastReceiver broadcastReceiver;
@@ -1148,7 +1149,7 @@ public void setRepeatMode(@RepeatMode final int repeatMode) {
simpleExoPlayer.setRepeatMode(repeatMode);
}
}
-
+
public void cycleNextRepeatMode() {
setRepeatMode(nextRepeatMode(getRepeatMode()));
}
@@ -1181,7 +1182,7 @@ public void onShuffleModeEnabledChanged(final boolean shuffleModeEnabled) {
UIs.call(playerUi -> playerUi.onShuffleModeEnabledChanged(shuffleModeEnabled));
notifyPlaybackUpdateToListeners();
}
-
+
public void toggleShuffleModeEnabled() {
if (!exoPlayerIsNull()) {
simpleExoPlayer.setShuffleModeEnabled(!simpleExoPlayer.getShuffleModeEnabled());
@@ -2301,6 +2302,7 @@ public Optional getFragmentListener() {
/**
* @return the user interfaces connected with the player
*/
+ @SuppressWarnings("MethodName") // keep the unusual method name
public PlayerUiList UIs() {
return UIs;
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/notification/NotificationUtil.java b/app/src/main/java/org/schabi/newpipe/player/notification/NotificationUtil.java
index 5f005245343..28c3b3655ad 100644
--- a/app/src/main/java/org/schabi/newpipe/player/notification/NotificationUtil.java
+++ b/app/src/main/java/org/schabi/newpipe/player/notification/NotificationUtil.java
@@ -132,7 +132,7 @@ private synchronized void updateNotification() {
// also update content intent, in case the user switched players
notificationBuilder.setContentIntent(PendingIntent.getActivity(player.getContext(),
- NOTIFICATION_ID, getIntentForNotification(player), FLAG_UPDATE_CURRENT));
+ NOTIFICATION_ID, getIntentForNotification(), FLAG_UPDATE_CURRENT));
notificationBuilder.setContentTitle(player.getVideoTitle());
notificationBuilder.setContentText(player.getUploaderName());
notificationBuilder.setTicker(player.getVideoTitle());
@@ -321,7 +321,7 @@ private NotificationCompat.Action getAction(@DrawableRes final int drawable,
new Intent(intentAction), FLAG_UPDATE_CURRENT));
}
- private Intent getIntentForNotification(final Player player) {
+ private Intent getIntentForNotification() {
if (player.audioPlayerSelected() || player.popupPlayerSelected()) {
// Means we play in popup or audio only. Let's show the play queue
return NavigationHelper.getPlayQueueActivityIntent(player.getContext());
diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/PlayerMediaSession.java b/app/src/main/java/org/schabi/newpipe/player/playback/PlayerMediaSession.java
index 2f261a0fa11..3be9b61734f 100644
--- a/app/src/main/java/org/schabi/newpipe/player/playback/PlayerMediaSession.java
+++ b/app/src/main/java/org/schabi/newpipe/player/playback/PlayerMediaSession.java
@@ -10,8 +10,6 @@
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.player.ui.VideoPlayerUi;
-import java.util.Optional;
-
public class PlayerMediaSession implements MediaSessionCallback {
private final Player player;
diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java
index c62382782e3..3bdda0029ae 100644
--- a/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java
+++ b/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java
@@ -497,7 +497,7 @@ private void showHideKodiButton() {
//region Captions (text tracks)
@Override
- protected void setupSubtitleView(float captionScale) {
+ protected void setupSubtitleView(final float captionScale) {
final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
final int minimumLength = Math.min(metrics.heightPixels, metrics.widthPixels);
final float captionRatioInverse = 20f + 4f * (1.0f - captionScale);
diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java
index c4db1f33426..49980069035 100644
--- a/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java
+++ b/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java
@@ -46,7 +46,7 @@ public Player getPlayer() {
/**
- * Called after the player received an intent and processed it
+ * Called after the player received an intent and processed it.
*/
public void setupAfterIntent() {
}
@@ -80,7 +80,7 @@ public void destroyPlayer() {
/**
* Called when this UI is being discarded, either because the player is switching to a different
- * UI or because the player is shutting down completely
+ * UI or because the player is shutting down completely.
*/
public void destroy() {
}
@@ -88,14 +88,14 @@ public void destroy() {
/**
* Called when the player is smooth-stopping, that is, transitioning smoothly to a new play
* queue after the user tapped on a new video stream while a stream was playing in the video
- * detail fragment
+ * detail fragment.
*/
public void smoothStopForImmediateReusing() {
}
/**
* Called when the video detail fragment listener is connected with the player, or right after
- * this UI is constructed if the listener is already connected then
+ * this UI is constructed if the listener is already connected then.
*/
public void onFragmentListenerSet() {
}
@@ -104,6 +104,7 @@ public void onFragmentListenerSet() {
* Broadcasts that the player receives will also be notified to UIs here. If you want to
* register new broadcast actions to receive here, add them to {@link
* Player#setupBroadcastReceiver()}.
+ * @param intent the broadcast intent received by the player
*/
public void onBroadcastReceived(final Intent intent) {
}
@@ -154,12 +155,14 @@ public void onMuteUnmuteChanged(final boolean isMuted) {
/**
* @see com.google.android.exoplayer2.Player.Listener#onTracksChanged(Tracks)
+ * @param currentTracks the available tracks information
*/
public void onTextTracksChanged(@NonNull final Tracks currentTracks) {
}
/**
* @see com.google.android.exoplayer2.Player.Listener#onPlaybackParametersChanged
+ * @param playbackParameters the new playback parameters
*/
public void onPlaybackParametersChanged(@NonNull final PlaybackParameters playbackParameters) {
}
@@ -172,12 +175,13 @@ public void onRenderedFirstFrame() {
/**
* @see com.google.android.exoplayer2.text.TextOutput#onCues
+ * @param cues the cues to pass to the subtitle view
*/
public void onCues(@NonNull final List cues) {
}
/**
- * Called when the stream being played changes
+ * Called when the stream being played changes.
* @param info the {@link StreamInfo} metadata object, along with data about the selected and
* available video streams (to be used to build the resolution menus, for example)
*/
@@ -185,7 +189,7 @@ public void onMetadataChanged(@NonNull final StreamInfo info) {
}
/**
- * Called when the thumbnail for the current metadata was loaded
+ * Called when the thumbnail for the current metadata was loaded.
* @param bitmap the thumbnail to process, or null if there is no thumbnail or there was an
* error when loading the thumbnail
*/
diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUiList.java b/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUiList.java
index 749cda02c61..05c0ed5b3cc 100644
--- a/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUiList.java
+++ b/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUiList.java
@@ -35,7 +35,7 @@ public void addAndPrepare(final PlayerUi playerUi) {
}
/**
- * Destroys all matching player UIs and removes them from the list
+ * Destroys all matching player UIs and removes them from the list.
* @param playerUiType the class of the player UI to destroy; the {@link
* Class#isInstance(Object)} method will be used, so even subclasses will be
* destroyed and removed
@@ -67,7 +67,7 @@ public Optional get(final Class playerUiType) {
}
/**
- * Calls the provided consumer on all player UIs in the list
+ * Calls the provided consumer on all player UIs in the list.
* @param consumer the consumer to call with player UIs
*/
public void call(final Consumer consumer) {
diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java
index f4ebc3304b5..393bf141bf4 100644
--- a/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java
+++ b/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java
@@ -86,7 +86,8 @@
import java.util.stream.Collectors;
public abstract class VideoPlayerUi extends PlayerUi
- implements SeekBar.OnSeekBarChangeListener, View.OnClickListener, View.OnLongClickListener, PopupMenu.OnMenuItemClickListener, PopupMenu.OnDismissListener {
+ implements SeekBar.OnSeekBarChangeListener, View.OnClickListener, View.OnLongClickListener,
+ PopupMenu.OnMenuItemClickListener, PopupMenu.OnDismissListener {
private static final String TAG = VideoPlayerUi.class.getSimpleName();
// time constants
@@ -476,7 +477,7 @@ private void updateEndScreenThumbnail() {
binding.endScreen.setImageBitmap(endScreenBitmap);
}
- protected abstract float calculateMaxEndScreenThumbnailHeight(@NonNull final Bitmap bitmap);
+ protected abstract float calculateMaxEndScreenThumbnailHeight(@NonNull Bitmap bitmap);
//endregion
@@ -511,6 +512,7 @@ public void onUpdateProgress(final int currentProgress,
/**
* Sets the current duration into the corresponding elements.
+ * @param currentProgress the current progress, in milliseconds
*/
private void updatePlayBackElementsCurrentDuration(final int currentProgress) {
// Don't set seekbar progress while user is seeking
@@ -522,6 +524,7 @@ private void updatePlayBackElementsCurrentDuration(final int currentProgress) {
/**
* 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) {
binding.playbackEndTime.setText(getTimeString(duration));
@@ -1214,7 +1217,7 @@ public boolean onMenuItemClick(@NonNull final MenuItem menuItem) {
final MediaItemTag.Quality quality = currentMetadata.getMaybeQuality().get();
final List availableStreams = quality.getSortedVideoStreams();
final int selectedStreamIndex = quality.getSelectedVideoStreamIndex();
- if (selectedStreamIndex == menuItemIndex|| availableStreams.size() <= menuItemIndex) {
+ if (selectedStreamIndex == menuItemIndex || availableStreams.size() <= menuItemIndex) {
return true;
}
@@ -1320,7 +1323,7 @@ public void onTextTracksChanged(@NonNull final Tracks currentTracks) {
}
@Override
- public void onCues(@NonNull List cues) {
+ public void onCues(@NonNull final List cues) {
super.onCues(cues);
binding.subtitleView.setCues(cues);
}
@@ -1332,7 +1335,7 @@ private void setupSubtitleView() {
binding.subtitleView.setStyle(captionStyle);
}
- protected abstract void setupSubtitleView(final float captionScale);
+ protected abstract void setupSubtitleView(float captionScale);
//endregion
@@ -1518,7 +1521,7 @@ void onResizeClicked() {
}
@Override
- public void onVideoSizeChanged(@NonNull VideoSize videoSize) {
+ public void onVideoSizeChanged(@NonNull final VideoSize videoSize) {
super.onVideoSizeChanged(videoSize);
binding.surfaceView.setAspectRatio(((float) videoSize.width) / videoSize.height);
}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/custom/NotificationActionsPreference.java b/app/src/main/java/org/schabi/newpipe/settings/custom/NotificationActionsPreference.java
index dfcf2e5974e..b4f6d598a43 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/custom/NotificationActionsPreference.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/custom/NotificationActionsPreference.java
@@ -28,7 +28,6 @@
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
-import org.schabi.newpipe.player.PlayerService;
import org.schabi.newpipe.player.notification.NotificationConstants;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ThemeHelper;
From 4979f84e4116d114dca851a31d706bec90a93450 Mon Sep 17 00:00:00 2001
From: Stypox
Date: Sat, 16 Apr 2022 16:01:23 +0200
Subject: [PATCH 131/240] Solve some Sonarlint warnings
---
.../newpipe/local/dialog/PlaylistDialog.java | 37 ++++++++++++++--
.../newpipe/player/PlayQueueActivity.java | 8 ++--
.../org/schabi/newpipe/player/Player.java | 42 +------------------
.../schabi/newpipe/player/PlayerService.java | 17 ++++----
.../gesture/BasePlayerGestureListener.kt | 5 ++-
.../player/notification/NotificationUtil.java | 1 +
.../newpipe/player/ui/MainPlayerUi.java | 13 +++---
.../schabi/newpipe/player/ui/PlayerUi.java | 2 +-
.../newpipe/player/ui/PopupPlayerUi.java | 16 ++++---
.../newpipe/player/ui/VideoPlayerUi.java | 40 +++++++++---------
10 files changed, 86 insertions(+), 95 deletions(-)
diff --git a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistDialog.java b/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistDialog.java
index f568ef81a03..dec8b05b230 100644
--- a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistDialog.java
+++ b/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistDialog.java
@@ -9,15 +9,20 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.FragmentManager;
import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
+import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.util.StateSaver;
import java.util.List;
+import java.util.Objects;
import java.util.Queue;
import java.util.function.Consumer;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.Disposable;
@@ -131,13 +136,13 @@ protected void setStreamEntities(final List streamEntities) {
* @param context context used for accessing the database
* @param streamEntities used for crating the dialog
* @param onExec execution that should occur after a dialog got created, e.g. showing it
- * @return Disposable
+ * @return the disposable that was created
*/
public static Disposable createCorrespondingDialog(
final Context context,
final List streamEntities,
- final Consumer onExec
- ) {
+ final Consumer onExec) {
+
return new LocalPlaylistManager(NewPipeDatabase.getInstance(context))
.hasPlaylists()
.observeOn(AndroidSchedulers.mainThread())
@@ -147,4 +152,30 @@ public static Disposable createCorrespondingDialog(
: PlaylistCreationDialog.newInstance(streamEntities))
);
}
+
+ /**
+ * Creates a {@link PlaylistAppendDialog} when playlists exists,
+ * otherwise a {@link PlaylistCreationDialog}. If the player's play queue is null or empty, no
+ * dialog will be created.
+ *
+ * @param player the player from which to extract the context and the play queue
+ * @param fragmentManager the fragment manager to use to show the dialog
+ * @return the disposable that was created
+ */
+ public static Disposable showForPlayQueue(
+ final Player player,
+ @NonNull final FragmentManager fragmentManager) {
+
+ final List streamEntities = Stream.of(player.getPlayQueue())
+ .filter(Objects::nonNull)
+ .flatMap(playQueue -> Objects.requireNonNull(playQueue).getStreams().stream())
+ .map(StreamEntity::new)
+ .collect(Collectors.toList());
+ if (streamEntities.isEmpty()) {
+ return Disposable.empty();
+ }
+
+ return PlaylistDialog.createCorrespondingDialog(player.getContext(), streamEntities,
+ dialog -> dialog.show(fragmentManager, "PlaylistDialog"));
+ }
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java b/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java
index cdba900f9bf..c18a7f4874d 100644
--- a/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java
@@ -29,6 +29,7 @@
import org.schabi.newpipe.databinding.ActivityPlayerQueueControlBinding;
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.playqueue.PlayQueue;
@@ -53,8 +54,6 @@ public final class PlayQueueActivity extends AppCompatActivity
private Player player;
- private PlayQueueAdapter adapter = null;
-
private boolean serviceBound;
private ServiceConnection serviceConnection;
@@ -128,7 +127,7 @@ public boolean onOptionsItemSelected(final MenuItem item) {
NavigationHelper.openSettings(this);
return true;
case R.id.action_append_playlist:
- player.onAddToPlaylistClicked(getSupportFragmentManager());
+ PlaylistDialog.showForPlayQueue(player, getSupportFragmentManager());
return true;
case R.id.action_playback_speed:
openPlaybackParameterDialog();
@@ -441,10 +440,9 @@ public void onStopTrackingTouch(final SeekBar seekBar) {
@Override
public void onQueueUpdate(@Nullable final PlayQueue queue) {
if (queue == null) {
- adapter = null;
queueControlBinding.playQueue.setAdapter(null);
} else {
- adapter = new PlayQueueAdapter(this, queue);
+ final PlayQueueAdapter adapter = new PlayQueueAdapter(this, queue);
adapter.setSelectedListener(getOnSelectedListener());
queueControlBinding.playQueue.setAdapter(adapter);
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java
index 0755f9b4de0..2d44c644918 100644
--- a/app/src/main/java/org/schabi/newpipe/player/Player.java
+++ b/app/src/main/java/org/schabi/newpipe/player/Player.java
@@ -63,16 +63,6 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.appcompat.content.res.AppCompatResources;
-import androidx.appcompat.view.ContextThemeWrapper;
-import androidx.appcompat.widget.AppCompatImageButton;
-import androidx.appcompat.widget.PopupMenu;
-import androidx.core.content.ContextCompat;
-import androidx.core.graphics.Insets;
-import androidx.core.view.ViewCompat;
-import androidx.core.view.WindowInsetsCompat;
-import androidx.fragment.app.FragmentManager;
import androidx.preference.PreferenceManager;
import com.google.android.exoplayer2.C;
@@ -96,7 +86,6 @@
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
-import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.databinding.PlayerBinding;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.ErrorUtil;
@@ -105,7 +94,6 @@
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
-import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.player.PlayerService.PlayerType;
import org.schabi.newpipe.player.event.PlayerEventListener;
@@ -116,6 +104,7 @@
import org.schabi.newpipe.player.helper.PlayerDataSource;
import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.player.mediaitem.MediaItemTag;
+import org.schabi.newpipe.player.notification.NotificationPlayerUi;
import org.schabi.newpipe.player.playback.MediaSourceManager;
import org.schabi.newpipe.player.playback.PlaybackListener;
import org.schabi.newpipe.player.playback.PlayerMediaSession;
@@ -125,7 +114,6 @@
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver;
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver.SourceType;
import org.schabi.newpipe.player.ui.MainPlayerUi;
-import org.schabi.newpipe.player.notification.NotificationPlayerUi;
import org.schabi.newpipe.player.ui.PlayerUi;
import org.schabi.newpipe.player.ui.PlayerUiList;
import org.schabi.newpipe.player.ui.PopupPlayerUi;
@@ -137,10 +125,8 @@
import org.schabi.newpipe.util.SerializedCache;
import org.schabi.newpipe.util.StreamTypeUtil;
-import java.util.Collections;
import java.util.List;
import java.util.Optional;
-import java.util.stream.Collectors;
import java.util.stream.IntStream;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
@@ -1192,32 +1178,6 @@ public void toggleShuffleModeEnabled() {
- /*//////////////////////////////////////////////////////////////////////////
- // Playlist append TODO this does not make sense here
- //////////////////////////////////////////////////////////////////////////*/
- //region Playlist append
-
- public void onAddToPlaylistClicked(@NonNull final FragmentManager fragmentManager) {
- if (DEBUG) {
- Log.d(TAG, "onAddToPlaylistClicked() called");
- }
-
- if (getPlayQueue() != null) {
- PlaylistDialog.createCorrespondingDialog(
- getContext(),
- getPlayQueue()
- .getStreams()
- .stream()
- .map(StreamEntity::new)
- .collect(Collectors.toList()),
- dialog -> dialog.show(fragmentManager, TAG)
- );
- }
- }
- //endregion
-
-
-
/*//////////////////////////////////////////////////////////////////////////
// Mute / Unmute
//////////////////////////////////////////////////////////////////////////*/
diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayerService.java b/app/src/main/java/org/schabi/newpipe/player/PlayerService.java
index b5014eeed09..326b0159006 100644
--- a/app/src/main/java/org/schabi/newpipe/player/PlayerService.java
+++ b/app/src/main/java/org/schabi/newpipe/player/PlayerService.java
@@ -71,16 +71,17 @@ public int onStartCommand(final Intent intent, final int flags, final int startI
Log.d(TAG, "onStartCommand() called with: intent = [" + intent
+ "], flags = [" + flags + "], startId = [" + startId + "]");
}
- if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
- && player.getPlayQueue() == null) {
- // Player is not working, no need to process media button's action
- return START_NOT_STICKY;
- }
- player.handleIntent(intent);
- if (player.getMediaSessionManager() != null) {
- player.getMediaSessionManager().handleMediaButtonIntent(intent);
+ if (!Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
+ || player.getPlayQueue() != null) {
+ // ^ no need to process media button's action if player is not working
+
+ player.handleIntent(intent);
+ if (player.getMediaSessionManager() != null) {
+ player.getMediaSessionManager().handleMediaButtonIntent(intent);
+ }
}
+
return START_NOT_STICKY;
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/gesture/BasePlayerGestureListener.kt b/app/src/main/java/org/schabi/newpipe/player/gesture/BasePlayerGestureListener.kt
index bd5d6f1c5f5..b006e73aac7 100644
--- a/app/src/main/java/org/schabi/newpipe/player/gesture/BasePlayerGestureListener.kt
+++ b/app/src/main/java/org/schabi/newpipe/player/gesture/BasePlayerGestureListener.kt
@@ -92,7 +92,10 @@ abstract class BasePlayerGestureListener(
return true
}
- return if (onDownNotDoubleTapping(e)) super.onDown(e) else true
+ if (onDownNotDoubleTapping(e)) {
+ return super.onDown(e)
+ }
+ return true
}
/**
diff --git a/app/src/main/java/org/schabi/newpipe/player/notification/NotificationUtil.java b/app/src/main/java/org/schabi/newpipe/player/notification/NotificationUtil.java
index 28c3b3655ad..2ba754500ff 100644
--- a/app/src/main/java/org/schabi/newpipe/player/notification/NotificationUtil.java
+++ b/app/src/main/java/org/schabi/newpipe/player/notification/NotificationUtil.java
@@ -266,6 +266,7 @@ private NotificationCompat.Action getAction(
null);
}
+ // fallthrough
case NotificationConstants.PLAY_PAUSE:
if (player.getCurrentState() == Player.STATE_COMPLETED) {
return getAction(R.drawable.ic_replay,
diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java
index 3bdda0029ae..eebcc81c464 100644
--- a/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java
+++ b/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java
@@ -51,6 +51,7 @@
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
import org.schabi.newpipe.info_list.StreamSegmentAdapter;
import org.schabi.newpipe.ktx.AnimationType;
+import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.event.PlayerServiceEventListener;
import org.schabi.newpipe.player.gesture.BasePlayerGestureListener;
@@ -147,7 +148,8 @@ protected void initListeners() {
binding.addToPlaylistButton.setOnClickListener(v ->
getParentActivity().map(FragmentActivity::getSupportFragmentManager)
- .ifPresent(player::onAddToPlaylistClicked));
+ .ifPresent(fragmentManager ->
+ PlaylistDialog.showForPlayQueue(player, fragmentManager)));
settingsContentObserver = new ContentObserver(new Handler()) {
@Override
@@ -401,6 +403,7 @@ public void onCompleted() {
//////////////////////////////////////////////////////////////////////////*/
//region Controls showing / hiding
+ @Override
protected void showOrHideButtons() {
super.showOrHideButtons();
@Nullable final PlayQueue playQueue = player.getPlayQueue();
@@ -667,12 +670,11 @@ public void closeItemsList() {
}
animate(binding.itemsListPanel, false, DEFAULT_CONTROLS_DURATION,
- AnimationType.SLIDE_AND_ALPHA, 0, () -> {
+ AnimationType.SLIDE_AND_ALPHA, 0, () ->
// Even when queueLayout is GONE it receives touch events
// and ruins normal behavior of the app. This line fixes it
binding.itemsListPanel.setTranslationY(
- -binding.itemsListPanel.getHeight() * 5);
- });
+ -binding.itemsListPanel.getHeight() * 5.0f));
// clear focus, otherwise a white rectangle remains on top of the player
binding.itemsListClose.clearFocus();
@@ -845,8 +847,7 @@ protected void onPlaybackSpeedClicked() {
}
PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), player.getPlaybackPitch(),
- player.getPlaybackSkipSilence(), (speed, pitch, skipSilence)
- -> player.setPlaybackParameters(speed, pitch, skipSilence))
+ player.getPlaybackSkipSilence(), player::setPlaybackParameters)
.show(activity.getSupportFragmentManager(), null);
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java
index 49980069035..9ce04bfd5c9 100644
--- a/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java
+++ b/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java
@@ -31,7 +31,7 @@ public abstract class PlayerUi {
/**
* @param player the player instance that will be usable throughout the lifetime of this UI
*/
- public PlayerUi(@NonNull final Player player) {
+ protected PlayerUi(@NonNull final Player player) {
this.context = player.getContext();
this.player = player;
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/PopupPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/PopupPlayerUi.java
index 43440b87359..8283437f88e 100644
--- a/app/src/main/java/org/schabi/newpipe/player/ui/PopupPlayerUi.java
+++ b/app/src/main/java/org/schabi/newpipe/player/ui/PopupPlayerUi.java
@@ -1,5 +1,6 @@
package org.schabi.newpipe.player.ui;
+import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
import static org.schabi.newpipe.MainActivity.DEBUG;
import static org.schabi.newpipe.player.helper.PlayerHelper.buildCloseOverlayLayoutParams;
import static org.schabi.newpipe.player.helper.PlayerHelper.getMinimumVideoHeight;
@@ -140,8 +141,7 @@ protected void setupElementsVisibility() {
binding.segmentsButton.setVisibility(View.GONE);
binding.moreOptionsButton.setVisibility(View.GONE);
binding.topControls.setOrientation(LinearLayout.HORIZONTAL);
- binding.primaryControls.getLayoutParams().width
- = LinearLayout.LayoutParams.WRAP_CONTENT;
+ binding.primaryControls.getLayoutParams().width = WRAP_CONTENT;
binding.secondaryControls.setAlpha(1.0f);
binding.secondaryControls.setVisibility(View.VISIBLE);
binding.secondaryControls.setTranslationY(0);
@@ -193,14 +193,12 @@ public void onBroadcastReceived(final Intent intent) {
updateScreenSize();
changePopupSize(popupLayoutParams.width);
checkPopupPositionBounds();
- } else if (Intent.ACTION_SCREEN_OFF.equals(intent.getAction())) {
- // Use only audio source when screen turns off while popup player is playing
- if (player.isPlaying() || player.isLoading()) {
+ } else if (player.isPlaying() || player.isLoading()) {
+ if (Intent.ACTION_SCREEN_OFF.equals(intent.getAction())) {
+ // Use only audio source when screen turns off while popup player is playing
player.useVideoSource(false);
- }
- } else if (Intent.ACTION_SCREEN_ON.equals(intent.getAction())) {
- // Restore video source when screen turns on and user is watching video in popup player
- if (player.isPlaying() || player.isLoading()) {
+ } else if (Intent.ACTION_SCREEN_ON.equals(intent.getAction())) {
+ // Restore video source when screen turns on and user was watching video in popup
player.useVideoSource(true);
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java
index 393bf141bf4..5b0be6f648c 100644
--- a/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java
+++ b/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java
@@ -41,7 +41,6 @@
import androidx.annotation.Nullable;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.appcompat.view.ContextThemeWrapper;
-import androidx.appcompat.widget.AppCompatImageButton;
import androidx.appcompat.widget.PopupMenu;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
@@ -142,7 +141,7 @@ public abstract class VideoPlayerUi extends PlayerUi
//////////////////////////////////////////////////////////////////////////*/
//region Constructor, setup, destroy
- public VideoPlayerUi(@NonNull final Player player,
+ protected VideoPlayerUi(@NonNull final Player player,
@NonNull final PlayerBinding playerBinding) {
super(player);
binding = playerBinding;
@@ -912,7 +911,20 @@ public void onShuffleClicked() {
@Override
public void onRepeatModeChanged(@RepeatMode final int repeatMode) {
super.onRepeatModeChanged(repeatMode);
- setRepeatModeButton(binding.repeatButton, repeatMode);
+
+ switch (repeatMode) {
+ case REPEAT_MODE_OFF:
+ binding.repeatButton.setImageResource(R.drawable.exo_controls_repeat_off);
+ break;
+ case REPEAT_MODE_ONE:
+ binding.repeatButton.setImageResource(R.drawable.exo_controls_repeat_one);
+ break;
+ case REPEAT_MODE_ALL:
+ binding.repeatButton.setImageResource(R.drawable.exo_controls_repeat_all);
+ break;
+ default:
+ break; // unreachable
+ }
}
@Override
@@ -927,21 +939,6 @@ public void onMuteUnmuteChanged(final boolean isMuted) {
setMuteButton(isMuted);
}
- private void setRepeatModeButton(final AppCompatImageButton imageButton,
- @RepeatMode final int repeatMode) {
- switch (repeatMode) {
- case REPEAT_MODE_OFF:
- imageButton.setImageResource(R.drawable.exo_controls_repeat_off);
- break;
- case REPEAT_MODE_ONE:
- imageButton.setImageResource(R.drawable.exo_controls_repeat_one);
- break;
- case REPEAT_MODE_ALL:
- imageButton.setImageResource(R.drawable.exo_controls_repeat_all);
- break;
- }
- }
-
private void setMuteButton(final boolean isMuted) {
binding.switchMute.setImageDrawable(AppCompatResources.getDrawable(context, isMuted
? R.drawable.ic_volume_off : R.drawable.ic_volume_up));
@@ -1037,6 +1034,7 @@ private void updateStreamRelatedViews() {
binding.qualityTextView.setVisibility(View.VISIBLE);
binding.surfaceView.setVisibility(View.VISIBLE);
+ // fallthrough
default:
binding.endScreen.setVisibility(View.GONE);
binding.playbackEndTime.setVisibility(View.VISIBLE);
@@ -1426,8 +1424,6 @@ public boolean onLongClick(final View v) {
public boolean onKeyDown(final int keyCode) {
switch (keyCode) {
- default:
- break;
case KeyEvent.KEYCODE_BACK:
if (DeviceUtils.isTv(context) && isControlsVisible()) {
hideControls(0, 0);
@@ -1442,7 +1438,7 @@ public boolean onKeyDown(final int keyCode) {
if ((binding.getRoot().hasFocus() && !binding.playbackControlRoot.hasFocus())
|| isAnyListViewOpen()) {
// do not interfere with focus in playlist and play queue etc.
- return false;
+ break;
}
if (player.getCurrentState() == org.schabi.newpipe.player.Player.STATE_BLOCKED) {
@@ -1458,6 +1454,8 @@ public boolean onKeyDown(final int keyCode) {
return true;
}
break;
+ default:
+ break; // ignore other keys
}
return false;
From 1cf746f7216c173194e17b52abb7bac85763cb23 Mon Sep 17 00:00:00 2001
From: Stypox
Date: Thu, 7 Jul 2022 11:09:07 +0200
Subject: [PATCH 132/240] Fix volume gestures not working anymore
---
.../gesture/MainPlayerGestureListener.kt | 55 ++++++++++++-------
.../newpipe/player/ui/MainPlayerUi.java | 7 ++-
2 files changed, 39 insertions(+), 23 deletions(-)
diff --git a/app/src/main/java/org/schabi/newpipe/player/gesture/MainPlayerGestureListener.kt b/app/src/main/java/org/schabi/newpipe/player/gesture/MainPlayerGestureListener.kt
index 81e216006c9..fd7b4ecf03f 100644
--- a/app/src/main/java/org/schabi/newpipe/player/gesture/MainPlayerGestureListener.kt
+++ b/app/src/main/java/org/schabi/newpipe/player/gesture/MainPlayerGestureListener.kt
@@ -8,11 +8,13 @@ import android.view.View.OnTouchListener
import android.widget.ProgressBar
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
+import androidx.core.view.isVisible
import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.R
import org.schabi.newpipe.ktx.AnimationType
import org.schabi.newpipe.ktx.animate
import org.schabi.newpipe.player.Player
+import org.schabi.newpipe.player.helper.AudioReactor
import org.schabi.newpipe.player.helper.PlayerHelper
import org.schabi.newpipe.player.ui.MainPlayerUi
import kotlin.math.abs
@@ -64,22 +66,27 @@ class MainPlayerGestureListener(
}
private fun onScrollVolume(distanceY: Float) {
+ val bar: ProgressBar = binding.volumeProgressBar
+ val audioReactor: AudioReactor = player.audioReactor
+
// If we just started sliding, change the progress bar to match the system volume
- if (binding.volumeRelativeLayout.visibility != View.VISIBLE) {
- val volumePercent: Float =
- player.audioReactor.volume / player.audioReactor.maxVolume.toFloat()
- binding.volumeProgressBar.progress = (volumePercent * MAX_GESTURE_LENGTH).toInt()
+ if (!binding.volumeRelativeLayout.isVisible) {
+ val volumePercent: Float = audioReactor.volume / audioReactor.maxVolume.toFloat()
+ bar.progress = (volumePercent * bar.max).toInt()
}
+ // Update progress bar
binding.volumeProgressBar.incrementProgressBy(distanceY.toInt())
- val currentProgressPercent: Float =
- binding.volumeProgressBar.progress.toFloat() / MAX_GESTURE_LENGTH
- val currentVolume = (player.audioReactor.maxVolume * currentProgressPercent).toInt()
- player.audioReactor.volume = currentVolume
+
+ // Update volume
+ val currentProgressPercent: Float = bar.progress / bar.max.toFloat()
+ val currentVolume = (audioReactor.maxVolume * currentProgressPercent).toInt()
+ audioReactor.volume = currentVolume
if (DEBUG) {
Log.d(TAG, "onScroll().volumeControl, currentVolume = $currentVolume")
}
+ // Update player center image
binding.volumeImageView.setImageDrawable(
AppCompatResources.getDrawable(
player.context,
@@ -92,12 +99,11 @@ class MainPlayerGestureListener(
)
)
- if (binding.volumeRelativeLayout.visibility != View.VISIBLE) {
+ // Make sure the correct layout is visible
+ if (!binding.volumeRelativeLayout.isVisible) {
binding.volumeRelativeLayout.animate(true, 200, AnimationType.SCALE_AND_ALPHA)
}
- if (binding.brightnessRelativeLayout.visibility == View.VISIBLE) {
- binding.volumeRelativeLayout.visibility = View.GONE
- }
+ binding.brightnessRelativeLayout.isVisible = false
}
private fun onScrollBrightness(distanceY: Float) {
@@ -105,9 +111,13 @@ class MainPlayerGestureListener(
val window = parent.window
val layoutParams = window.attributes
val bar: ProgressBar = binding.brightnessProgressBar
+
+ // Update progress bar
val oldBrightness = layoutParams.screenBrightness
bar.progress = (bar.max * max(0f, min(1f, oldBrightness))).toInt()
bar.incrementProgressBy(distanceY.toInt())
+
+ // Update brightness
val currentProgressPercent = bar.progress.toFloat() / bar.max
layoutParams.screenBrightness = currentProgressPercent
window.attributes = layoutParams
@@ -121,26 +131,32 @@ class MainPlayerGestureListener(
"currentBrightness = " + currentProgressPercent
)
}
+
+ // Update player center image
binding.brightnessImageView.setImageDrawable(
AppCompatResources.getDrawable(
player.context,
- if (currentProgressPercent < 0.25) R.drawable.ic_brightness_low else if (currentProgressPercent < 0.75) R.drawable.ic_brightness_medium else R.drawable.ic_brightness_high
+ when {
+ currentProgressPercent < 0.25 -> R.drawable.ic_brightness_low
+ currentProgressPercent < 0.75 -> R.drawable.ic_brightness_medium
+ else -> R.drawable.ic_brightness_high
+ }
)
)
- if (binding.brightnessRelativeLayout.visibility != View.VISIBLE) {
+
+ // Make sure the correct layout is visible
+ if (!binding.brightnessRelativeLayout.isVisible) {
binding.brightnessRelativeLayout.animate(true, 200, AnimationType.SCALE_AND_ALPHA)
}
- if (binding.volumeRelativeLayout.visibility == View.VISIBLE) {
- binding.volumeRelativeLayout.visibility = View.GONE
- }
+ binding.volumeRelativeLayout.isVisible = false
}
override fun onScrollEnd(event: MotionEvent) {
super.onScrollEnd(event)
- if (binding.volumeRelativeLayout.visibility == View.VISIBLE) {
+ if (binding.volumeRelativeLayout.isVisible) {
binding.volumeRelativeLayout.animate(false, 200, AnimationType.SCALE_AND_ALPHA, 200)
}
- if (binding.brightnessRelativeLayout.visibility == View.VISIBLE) {
+ if (binding.brightnessRelativeLayout.isVisible) {
binding.brightnessRelativeLayout.animate(false, 200, AnimationType.SCALE_AND_ALPHA, 200)
}
}
@@ -210,7 +226,6 @@ class MainPlayerGestureListener(
private val TAG = MainPlayerGestureListener::class.java.simpleName
private val DEBUG = MainActivity.DEBUG
private const val MOVEMENT_THRESHOLD = 40
- const val MAX_GESTURE_LENGTH = 0.75f
private fun getNavigationBarHeight(context: Context): Int {
val resId = context.resources
diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java
index eebcc81c464..d9f5ea7f43c 100644
--- a/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java
+++ b/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java
@@ -520,12 +520,13 @@ protected void setupSubtitleView(final float captionScale) {
public void onLayoutChange(final View view, final int l, final int t, final int r, final int b,
final int ol, final int ot, final int or, final int ob) {
if (l != ol || t != ot || r != or || b != ob) {
- // Use smaller value to be consistent between screen orientations
- // (and to make usage easier)
+ // Use a smaller value to be consistent across screen orientations, and to make usage
+ // easier. Multiply by 3/4 to ensure the user does not need to move the finger up to the
+ // screen border, in order to reach the maximum volume/brightness.
final int width = r - l;
final int height = b - t;
final int min = Math.min(width, height);
- final int maxGestureLength = (int) (min * MainPlayerGestureListener.MAX_GESTURE_LENGTH);
+ final int maxGestureLength = (int) (min * 0.75);
if (DEBUG) {
Log.d(TAG, "maxGestureLength = " + maxGestureLength);
From 9c51fc3adeaac4670da0bead156315b286f8b5c3 Mon Sep 17 00:00:00 2001
From: Stypox
Date: Thu, 7 Jul 2022 11:59:00 +0200
Subject: [PATCH 133/240] Move functions to get Android dimen to ThemeHelper
---
.../gesture/MainPlayerGestureListener.kt | 28 ++++++-------------
.../org/schabi/newpipe/util/ThemeHelper.java | 16 +++++++++++
2 files changed, 24 insertions(+), 20 deletions(-)
diff --git a/app/src/main/java/org/schabi/newpipe/player/gesture/MainPlayerGestureListener.kt b/app/src/main/java/org/schabi/newpipe/player/gesture/MainPlayerGestureListener.kt
index fd7b4ecf03f..095b3ccdb77 100644
--- a/app/src/main/java/org/schabi/newpipe/player/gesture/MainPlayerGestureListener.kt
+++ b/app/src/main/java/org/schabi/newpipe/player/gesture/MainPlayerGestureListener.kt
@@ -1,6 +1,5 @@
package org.schabi.newpipe.player.gesture
-import android.content.Context
import android.util.Log
import android.view.MotionEvent
import android.view.View
@@ -17,6 +16,7 @@ import org.schabi.newpipe.player.Player
import org.schabi.newpipe.player.helper.AudioReactor
import org.schabi.newpipe.player.helper.PlayerHelper
import org.schabi.newpipe.player.ui.MainPlayerUi
+import org.schabi.newpipe.util.ThemeHelper.getAndroidDimenPx
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
@@ -172,9 +172,13 @@ class MainPlayerGestureListener(
return false
}
- val isTouchingStatusBar: Boolean = initialEvent.y < getStatusBarHeight(player.context)
- val isTouchingNavigationBar: Boolean =
- initialEvent.y > (binding.root.height - getNavigationBarHeight(player.context))
+ // Calculate heights of status and navigation bars
+ val statusBarHeight = getAndroidDimenPx(player.context, "status_bar_height")
+ val navigationBarHeight = getAndroidDimenPx(player.context, "navigation_bar_height")
+
+ // Do not handle this event if initially it started from status or navigation bars
+ val isTouchingStatusBar = initialEvent.y < statusBarHeight
+ val isTouchingNavigationBar = initialEvent.y > (binding.root.height - navigationBarHeight)
if (isTouchingStatusBar || isTouchingNavigationBar) {
return false
}
@@ -226,21 +230,5 @@ class MainPlayerGestureListener(
private val TAG = MainPlayerGestureListener::class.java.simpleName
private val DEBUG = MainActivity.DEBUG
private const val MOVEMENT_THRESHOLD = 40
-
- private fun getNavigationBarHeight(context: Context): Int {
- val resId = context.resources
- .getIdentifier("navigation_bar_height", "dimen", "android")
- return if (resId > 0) {
- context.resources.getDimensionPixelSize(resId)
- } else 0
- }
-
- private fun getStatusBarHeight(context: Context): Int {
- val resId = context.resources
- .getIdentifier("status_bar_height", "dimen", "android")
- return if (resId > 0) {
- context.resources.getDimensionPixelSize(resId)
- } else 0
- }
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java b/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java
index b8e3a86ed38..389af80eefe 100644
--- a/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java
@@ -244,6 +244,22 @@ public static Drawable resolveDrawable(@NonNull final Context context,
return AppCompatResources.getDrawable(context, typedValue.resourceId);
}
+ /**
+ * Gets a runtime dimen from the {@code android} package. Should be used for dimens for which
+ * normal accessing with {@code R.dimen.} is not available.
+ *
+ * @param context context
+ * @param name dimen resource name (e.g. navigation_bar_height)
+ * @return the obtained dimension, in pixels, or 0 if the resource could not be resolved
+ */
+ public static int getAndroidDimenPx(@NonNull final Context context, final String name) {
+ final int resId = context.getResources().getIdentifier(name, "dimen", "android");
+ if (resId <= 0) {
+ return 0;
+ }
+ return context.getResources().getDimensionPixelSize(resId);
+ }
+
private static String getSelectedThemeKey(final Context context) {
final String themeKey = context.getString(R.string.theme_key);
final String defaultTheme = context.getResources().getString(R.string.default_theme_value);
From 3692858a3d10fb33d0386149d630264ca93eca23 Mon Sep 17 00:00:00 2001
From: Stypox
Date: Fri, 8 Jul 2022 22:33:35 +0200
Subject: [PATCH 134/240] Move popup layout param to PopupPlayerUi
---
.../gesture/PopupPlayerGestureListener.kt | 65 ++++++-----
.../newpipe/player/helper/PlayerHelper.java | 103 +---------------
.../newpipe/player/ui/PopupPlayerUi.java | 110 +++++++++++++++++-
3 files changed, 141 insertions(+), 137 deletions(-)
diff --git a/app/src/main/java/org/schabi/newpipe/player/gesture/PopupPlayerGestureListener.kt b/app/src/main/java/org/schabi/newpipe/player/gesture/PopupPlayerGestureListener.kt
index b8c1bc54c5f..bda6ee8d10f 100644
--- a/app/src/main/java/org/schabi/newpipe/player/gesture/PopupPlayerGestureListener.kt
+++ b/app/src/main/java/org/schabi/newpipe/player/gesture/PopupPlayerGestureListener.kt
@@ -7,7 +7,6 @@ import android.view.ViewConfiguration
import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.ktx.AnimationType
import org.schabi.newpipe.ktx.animate
-import org.schabi.newpipe.player.helper.PlayerHelper
import org.schabi.newpipe.player.ui.PopupPlayerUi
import kotlin.math.abs
import kotlin.math.hypot
@@ -87,7 +86,7 @@ class PopupPlayerGestureListener(
player.changeState(player.currentState)
}
if (!playerUi.isPopupClosing) {
- PlayerHelper.savePopupPositionAndSizeToPrefs(playerUi)
+ playerUi.savePopupPositionAndSizeToPrefs()
}
}
@@ -106,40 +105,42 @@ class PopupPlayerGestureListener(
}
private fun handleMultiDrag(event: MotionEvent): Boolean {
- if (initPointerDistance != -1.0 && event.pointerCount == 2) {
- // get the movements of the fingers
- val firstPointerMove = hypot(
- event.getX(0) - initFirstPointerX.toDouble(),
- event.getY(0) - initFirstPointerY.toDouble()
- )
- val secPointerMove = hypot(
- event.getX(1) - initSecPointerX.toDouble(),
- event.getY(1) - initSecPointerY.toDouble()
- )
+ if (initPointerDistance == -1.0 || event.pointerCount != 2) {
+ return false
+ }
- // minimum threshold beyond which pinch gesture will work
- val minimumMove = ViewConfiguration.get(player.context).scaledTouchSlop
+ // get the movements of the fingers
+ val firstPointerMove = hypot(
+ event.getX(0) - initFirstPointerX.toDouble(),
+ event.getY(0) - initFirstPointerY.toDouble()
+ )
+ val secPointerMove = hypot(
+ event.getX(1) - initSecPointerX.toDouble(),
+ event.getY(1) - initSecPointerY.toDouble()
+ )
+
+ // minimum threshold beyond which pinch gesture will work
+ val minimumMove = ViewConfiguration.get(player.context).scaledTouchSlop
+ if (max(firstPointerMove, secPointerMove) <= minimumMove) {
+ return false
+ }
- if (max(firstPointerMove, secPointerMove) > minimumMove) {
- // calculate current distance between the pointers
- val currentPointerDistance = hypot(
- event.getX(0) - event.getX(1).toDouble(),
- event.getY(0) - event.getY(1).toDouble()
- )
+ // calculate current distance between the pointers
+ val currentPointerDistance = hypot(
+ event.getX(0) - event.getX(1).toDouble(),
+ event.getY(0) - event.getY(1).toDouble()
+ )
- val popupWidth = playerUi.popupLayoutParams.width.toDouble()
- // change co-ordinates of popup so the center stays at the same position
- val newWidth = popupWidth * currentPointerDistance / initPointerDistance
- initPointerDistance = currentPointerDistance
- playerUi.popupLayoutParams.x += ((popupWidth - newWidth) / 2.0).toInt()
+ val popupWidth = playerUi.popupLayoutParams.width.toDouble()
+ // change co-ordinates of popup so the center stays at the same position
+ val newWidth = popupWidth * currentPointerDistance / initPointerDistance
+ initPointerDistance = currentPointerDistance
+ playerUi.popupLayoutParams.x += ((popupWidth - newWidth) / 2.0).toInt()
- playerUi.checkPopupPositionBounds()
- playerUi.updateScreenSize()
- playerUi.changePopupSize(min(playerUi.screenWidth.toDouble(), newWidth).toInt())
- return true
- }
- }
- return false
+ playerUi.checkPopupPositionBounds()
+ playerUi.updateScreenSize()
+ playerUi.changePopupSize(min(playerUi.screenWidth.toDouble(), newWidth).toInt())
+ return true
}
private fun onPopupResizingStart() {
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java
index ec4cf8602a5..d1d29dd71a1 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java
@@ -10,19 +10,13 @@
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND;
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE;
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_POPUP;
-import static org.schabi.newpipe.player.ui.PopupPlayerUi.IDLE_WINDOW_FLAGS;
import static java.lang.annotation.RetentionPolicy.SOURCE;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
-import android.graphics.PixelFormat;
-import android.os.Build;
import android.provider.Settings;
-import android.view.Gravity;
-import android.view.ViewGroup;
-import android.view.WindowManager;
import android.view.accessibility.CaptioningManager;
import androidx.annotation.IntDef;
@@ -49,12 +43,11 @@
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
import org.schabi.newpipe.extractor.utils.Utils;
-import org.schabi.newpipe.player.PlayerService;
import org.schabi.newpipe.player.Player;
+import org.schabi.newpipe.player.PlayerService;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
-import org.schabi.newpipe.player.ui.PopupPlayerUi;
import org.schabi.newpipe.util.ListHelper;
import java.lang.annotation.Retention;
@@ -77,20 +70,6 @@ public final class PlayerHelper {
private static final NumberFormat SPEED_FORMATTER = new DecimalFormat("0.##x");
private static final NumberFormat PITCH_FORMATTER = new DecimalFormat("##%");
- /**
- * Maximum opacity allowed for Android 12 and higher to allow touches on other apps when using
- * NewPipe's popup player.
- *
- *
- * This value is hardcoded instead of being get dynamically with the method linked of the
- * constant documentation below, because it is not static and popup player layout parameters
- * are generated with static methods.
- *
- *
- * @see WindowManager.LayoutParams#FLAG_NOT_TOUCHABLE
- */
- private static final float MAXIMUM_OPACITY_ALLOWED_FOR_S_AND_HIGHER = 0.8f;
-
@Retention(SOURCE)
@IntDef({AUTOPLAY_TYPE_ALWAYS, AUTOPLAY_TYPE_WIFI,
AUTOPLAY_TYPE_NEVER})
@@ -525,90 +504,10 @@ public static void savePlaybackParametersToPrefs(final Player player,
.apply();
}
- /**
- * @param playerUi {@code screenWidth} and {@code screenHeight} must have been initialized
- * @return the popup starting layout params
- */
- @SuppressLint("RtlHardcoded")
- public static WindowManager.LayoutParams retrievePopupLayoutParamsFromPrefs(
- final PopupPlayerUi playerUi) {
- final SharedPreferences prefs = playerUi.getPlayer().getPrefs();
- final Context context = playerUi.getPlayer().getContext();
-
- final boolean popupRememberSizeAndPos = prefs.getBoolean(
- context.getString(R.string.popup_remember_size_pos_key), true);
- final float defaultSize = context.getResources().getDimension(R.dimen.popup_default_width);
- final float popupWidth = popupRememberSizeAndPos
- ? prefs.getFloat(context.getString(R.string.popup_saved_width_key), defaultSize)
- : defaultSize;
- final float popupHeight = getMinimumVideoHeight(popupWidth);
-
- final WindowManager.LayoutParams popupLayoutParams = new WindowManager.LayoutParams(
- (int) popupWidth, (int) popupHeight,
- popupLayoutParamType(),
- IDLE_WINDOW_FLAGS,
- PixelFormat.TRANSLUCENT);
- popupLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
- popupLayoutParams.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
-
- final int centerX = (int) (playerUi.getScreenWidth() / 2f - popupWidth / 2f);
- final int centerY = (int) (playerUi.getScreenHeight() / 2f - popupHeight / 2f);
- popupLayoutParams.x = popupRememberSizeAndPos
- ? prefs.getInt(context.getString(R.string.popup_saved_x_key), centerX) : centerX;
- popupLayoutParams.y = popupRememberSizeAndPos
- ? prefs.getInt(context.getString(R.string.popup_saved_y_key), centerY) : centerY;
-
- return popupLayoutParams;
- }
-
- public static void savePopupPositionAndSizeToPrefs(final PopupPlayerUi playerUi) {
- if (playerUi.getPopupLayoutParams() != null) {
- final Context context = playerUi.getPlayer().getContext();
- playerUi.getPlayer().getPrefs().edit()
- .putFloat(context.getString(R.string.popup_saved_width_key),
- playerUi.getPopupLayoutParams().width)
- .putInt(context.getString(R.string.popup_saved_x_key),
- playerUi.getPopupLayoutParams().x)
- .putInt(context.getString(R.string.popup_saved_y_key),
- playerUi.getPopupLayoutParams().y)
- .apply();
- }
- }
-
public static float getMinimumVideoHeight(final float width) {
return width / (16.0f / 9.0f); // Respect the 16:9 ratio that most videos have
}
- @SuppressLint("RtlHardcoded")
- public static WindowManager.LayoutParams buildCloseOverlayLayoutParams() {
- final int flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
- | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
- | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
-
- final WindowManager.LayoutParams closeOverlayLayoutParams = new WindowManager.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
- popupLayoutParamType(),
- flags,
- PixelFormat.TRANSLUCENT);
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- // Setting maximum opacity allowed for touch events to other apps for Android 12 and
- // higher to prevent non interaction when using other apps with the popup player
- closeOverlayLayoutParams.alpha = MAXIMUM_OPACITY_ALLOWED_FOR_S_AND_HIGHER;
- }
-
- closeOverlayLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
- closeOverlayLayoutParams.softInputMode =
- WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
- return closeOverlayLayoutParams;
- }
-
- public static int popupLayoutParamType() {
- return Build.VERSION.SDK_INT < Build.VERSION_CODES.O
- ? WindowManager.LayoutParams.TYPE_PHONE
- : WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
- }
-
public static int retrieveSeekDurationFromPreferences(final Player player) {
return Integer.parseInt(Objects.requireNonNull(player.getPrefs().getString(
player.getContext().getString(R.string.seek_duration_key),
diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/PopupPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/PopupPlayerUi.java
index 8283437f88e..46396a84036 100644
--- a/app/src/main/java/org/schabi/newpipe/player/ui/PopupPlayerUi.java
+++ b/app/src/main/java/org/schabi/newpipe/player/ui/PopupPlayerUi.java
@@ -2,21 +2,25 @@
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
import static org.schabi.newpipe.MainActivity.DEBUG;
-import static org.schabi.newpipe.player.helper.PlayerHelper.buildCloseOverlayLayoutParams;
import static org.schabi.newpipe.player.helper.PlayerHelper.getMinimumVideoHeight;
-import static org.schabi.newpipe.player.helper.PlayerHelper.retrievePopupLayoutParamsFromPrefs;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.annotation.SuppressLint;
+import android.content.Context;
import android.content.Intent;
+import android.content.SharedPreferences;
import android.content.res.Resources;
import android.graphics.Bitmap;
+import android.graphics.PixelFormat;
+import android.os.Build;
import android.util.DisplayMetrics;
import android.util.Log;
+import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
+import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.animation.AnticipateInterpolator;
import android.widget.LinearLayout;
@@ -38,6 +42,20 @@
public final class PopupPlayerUi extends VideoPlayerUi {
private static final String TAG = PopupPlayerUi.class.getSimpleName();
+ /**
+ * Maximum opacity allowed for Android 12 and higher to allow touches on other apps when using
+ * NewPipe's popup player.
+ *
+ *
+ * This value is hardcoded instead of being get dynamically with the method linked of the
+ * constant documentation below, because it is not static and popup player layout parameters
+ * are generated with static methods.
+ *
+ *
+ * @see WindowManager.LayoutParams#FLAG_NOT_TOUCHABLE
+ */
+ private static final float MAXIMUM_OPACITY_ALLOWED_FOR_S_AND_HIGHER = 0.8f;
+
/*//////////////////////////////////////////////////////////////////////////
// Popup player
//////////////////////////////////////////////////////////////////////////*/
@@ -98,7 +116,7 @@ private void initPopup() {
updateScreenSize();
- popupLayoutParams = retrievePopupLayoutParamsFromPrefs(this);
+ popupLayoutParams = retrievePopupLayoutParamsFromPrefs();
binding.surfaceView.setHeights(popupLayoutParams.height, popupLayoutParams.height);
checkPopupPositionBounds();
@@ -446,6 +464,92 @@ public boolean isInsideClosingRadius(@NonNull final MotionEvent popupMotionEvent
//endregion
+ /*//////////////////////////////////////////////////////////////////////////
+ // Popup & closing overlay layout params + saving popup position and size
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Popup & closing overlay layout params + saving popup position and size
+
+ /**
+ * {@code screenWidth} and {@code screenHeight} must have been initialized.
+ * @return the popup starting layout params
+ */
+ @SuppressLint("RtlHardcoded")
+ public WindowManager.LayoutParams retrievePopupLayoutParamsFromPrefs() {
+ final SharedPreferences prefs = getPlayer().getPrefs();
+ final Context context = getPlayer().getContext();
+
+ final boolean popupRememberSizeAndPos = prefs.getBoolean(
+ context.getString(R.string.popup_remember_size_pos_key), true);
+ final float defaultSize = context.getResources().getDimension(R.dimen.popup_default_width);
+ final float popupWidth = popupRememberSizeAndPos
+ ? prefs.getFloat(context.getString(R.string.popup_saved_width_key), defaultSize)
+ : defaultSize;
+ final float popupHeight = getMinimumVideoHeight(popupWidth);
+
+ final WindowManager.LayoutParams params = new WindowManager.LayoutParams(
+ (int) popupWidth, (int) popupHeight,
+ popupLayoutParamType(),
+ IDLE_WINDOW_FLAGS,
+ PixelFormat.TRANSLUCENT);
+ params.gravity = Gravity.LEFT | Gravity.TOP;
+ params.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
+
+ final int centerX = (int) (screenWidth / 2f - popupWidth / 2f);
+ final int centerY = (int) (screenHeight / 2f - popupHeight / 2f);
+ params.x = popupRememberSizeAndPos
+ ? prefs.getInt(context.getString(R.string.popup_saved_x_key), centerX) : centerX;
+ params.y = popupRememberSizeAndPos
+ ? prefs.getInt(context.getString(R.string.popup_saved_y_key), centerY) : centerY;
+
+ return params;
+ }
+
+ public void savePopupPositionAndSizeToPrefs() {
+ if (getPopupLayoutParams() != null) {
+ final Context context = getPlayer().getContext();
+ getPlayer().getPrefs().edit()
+ .putFloat(context.getString(R.string.popup_saved_width_key),
+ popupLayoutParams.width)
+ .putInt(context.getString(R.string.popup_saved_x_key),
+ popupLayoutParams.x)
+ .putInt(context.getString(R.string.popup_saved_y_key),
+ popupLayoutParams.y)
+ .apply();
+ }
+ }
+
+ @SuppressLint("RtlHardcoded")
+ public static WindowManager.LayoutParams buildCloseOverlayLayoutParams() {
+ final int flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+ | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
+ | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
+
+ final WindowManager.LayoutParams closeOverlayLayoutParams = new WindowManager.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
+ popupLayoutParamType(),
+ flags,
+ PixelFormat.TRANSLUCENT);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ // Setting maximum opacity allowed for touch events to other apps for Android 12 and
+ // higher to prevent non interaction when using other apps with the popup player
+ closeOverlayLayoutParams.alpha = MAXIMUM_OPACITY_ALLOWED_FOR_S_AND_HIGHER;
+ }
+
+ closeOverlayLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
+ closeOverlayLayoutParams.softInputMode =
+ WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
+ return closeOverlayLayoutParams;
+ }
+
+ public static int popupLayoutParamType() {
+ return Build.VERSION.SDK_INT < Build.VERSION_CODES.O
+ ? WindowManager.LayoutParams.TYPE_PHONE
+ : WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
+ }
+ //endregion
+
+
/*//////////////////////////////////////////////////////////////////////////
// Getters
//////////////////////////////////////////////////////////////////////////*/
From 61c1da144e4b0a824d4816d9ecad6a59b53d9cf3 Mon Sep 17 00:00:00 2001
From: Stypox
Date: Sat, 9 Jul 2022 17:17:30 +0200
Subject: [PATCH 135/240] Some refactorings after review comments
---
.../fragments/detail/VideoDetailFragment.java | 11 +++--
.../newpipe/local/dialog/PlaylistDialog.java | 2 +-
.../schabi/newpipe/player/PlayerService.java | 23 ++++-----
.../newpipe/player/ui/MainPlayerUi.java | 47 ++++++++-----------
.../newpipe/player/ui/VideoPlayerUi.java | 19 +++-----
5 files changed, 42 insertions(+), 60 deletions(-)
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java
index 8ffff2f9ef1..5dc6bb436de 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java
@@ -1311,11 +1311,12 @@ private void addVideoPlayerView() {
setHeightThumbnail();
// Prevent from re-adding a view multiple times
- new Handler().post(() -> player.UIs().get(MainPlayerUi.class).ifPresent(playerUi -> {
- playerUi.removeViewFromParent();
- binding.playerPlaceholder.addView(playerUi.getBinding().getRoot());
- playerUi.setupVideoSurfaceIfNeeded();
- }));
+ new Handler(Looper.getMainLooper()).post(() ->
+ player.UIs().get(MainPlayerUi.class).ifPresent(playerUi -> {
+ playerUi.removeViewFromParent();
+ binding.playerPlaceholder.addView(playerUi.getBinding().getRoot());
+ playerUi.setupVideoSurfaceIfNeeded();
+ }));
}
private void removeVideoPlayerView() {
diff --git a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistDialog.java b/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistDialog.java
index dec8b05b230..612c3818187 100644
--- a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistDialog.java
+++ b/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistDialog.java
@@ -168,7 +168,7 @@ public static Disposable showForPlayQueue(
final List streamEntities = Stream.of(player.getPlayQueue())
.filter(Objects::nonNull)
- .flatMap(playQueue -> Objects.requireNonNull(playQueue).getStreams().stream())
+ .flatMap(playQueue -> playQueue.getStreams().stream())
.map(StreamEntity::new)
.collect(Collectors.toList());
if (streamEntities.isEmpty()) {
diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayerService.java b/app/src/main/java/org/schabi/newpipe/player/PlayerService.java
index 326b0159006..14e8262d6fb 100644
--- a/app/src/main/java/org/schabi/newpipe/player/PlayerService.java
+++ b/app/src/main/java/org/schabi/newpipe/player/PlayerService.java
@@ -33,8 +33,6 @@
/**
* One service for all players.
- *
- * @author mauriciocolli
*/
public final class PlayerService extends Service {
private static final String TAG = PlayerService.class.getSimpleName();
@@ -72,14 +70,16 @@ public int onStartCommand(final Intent intent, final int flags, final int startI
+ "], flags = [" + flags + "], startId = [" + startId + "]");
}
- if (!Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
- || player.getPlayQueue() != null) {
- // ^ no need to process media button's action if player is not working
+ if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
+ && player.getPlayQueue() == null) {
+ // No need to process media button's actions if the player is not working, otherwise the
+ // player service would strangely start with nothing to play
+ return START_NOT_STICKY;
+ }
- player.handleIntent(intent);
- if (player.getMediaSessionManager() != null) {
- player.getMediaSessionManager().handleMediaButtonIntent(intent);
- }
+ player.handleIntent(intent);
+ if (player.getMediaSessionManager() != null) {
+ player.getMediaSessionManager().handleMediaButtonIntent(intent);
}
return START_NOT_STICKY;
@@ -97,11 +97,6 @@ public void stopForImmediateReusing() {
// We can't just pause the player here because it will make transition
// from one stream to a new stream not smooth
player.smoothStopForImmediateReusing();
-
- // Notification shows information about old stream but if a user selects
- // a stream from backStack it's not actual anymore
- // So we should hide the notification at all.
- // When autoplay enabled such notification flashing is annoying so skip this case
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java
index d9f5ea7f43c..278e4f1fffa 100644
--- a/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java
+++ b/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java
@@ -75,6 +75,11 @@
public final class MainPlayerUi extends VideoPlayerUi implements View.OnLayoutChangeListener {
private static final String TAG = MainPlayerUi.class.getSimpleName();
+ // see the Javadoc of calculateMaxEndScreenThumbnailHeight for information
+ private static final int DETAIL_ROOT_MINIMUM_HEIGHT = 85; // dp
+ private static final int DETAIL_TITLE_TEXT_SIZE_TV = 16; // sp
+ private static final int DETAIL_TITLE_TEXT_SIZE_TABLET = 15; // sp
+
private boolean isFullscreen = false;
private boolean isVerticalVideo = false;
private boolean fragmentIsVisible = false;
@@ -262,13 +267,8 @@ protected void setupElementsVisibility() {
binding.topControls.setClickable(true);
binding.topControls.setFocusable(true);
- if (isFullscreen) {
- binding.titleTextView.setVisibility(View.VISIBLE);
- binding.channelTextView.setVisibility(View.VISIBLE);
- } else {
- binding.titleTextView.setVisibility(View.GONE);
- binding.channelTextView.setVisibility(View.GONE);
- }
+ binding.titleTextView.setVisibility(isFullscreen ? View.VISIBLE : View.GONE);
+ binding.channelTextView.setVisibility(isFullscreen ? View.VISIBLE : View.GONE);
}
@Override
@@ -450,13 +450,12 @@ public void hideSystemUIIfNeeded() {
* The calculating follows these rules:
*
*
- * Show at least stream title and content creator on TVs and tablets
- * when in landscape (always the case for TVs) and not in fullscreen mode.
- * This requires to have at least 85dp free space for {@link R.id.detail_root}
- * and additional space for the stream title text size
- * ({@link R.id.detail_title_root_layout}).
- * The text size is 15sp on tablets and 16sp on TVs,
- * see {@link R.id.titleTextView}.
+ * Show at least stream title and content creator on TVs and tablets when in landscape
+ * (always the case for TVs) and not in fullscreen mode. This requires to have at least
+ * {@link #DETAIL_ROOT_MINIMUM_HEIGHT} free space for {@link R.id.detail_root} and
+ * additional space for the stream title text size ({@link R.id.detail_title_root_layout}).
+ * The text size is {@link #DETAIL_TITLE_TEXT_SIZE_TABLET} on tablets and
+ * {@link #DETAIL_TITLE_TEXT_SIZE_TV} on TVs, see {@link R.id.titleTextView}.
*