-Added playback speed control dialog to allow full user control over player tempo and pitch parameters.

-Changed tempo and pitch button in service player activity and tempo button in main video player to open speed control dialog.
-Changed LIVE button to be no longer clickable when player position is at or beyond default position.
-Changed main video player to use AppCompatActivity rather than Activity.
-Fixed video player tempo button not updating when player speed parameters change.
-Fixed player crashing on lower sdk versions due to no MediaButtonReceiver, added intent back to manifest.
-Fixed inconsistent gradle library naming.
-Fixed stetho dependencies incorrect version.
This commit is contained in:
John Zhen Mo 2018-03-21 00:11:54 -07:00
parent 5167fe078b
commit e885822a34
9 changed files with 755 additions and 62 deletions

View File

@ -51,9 +51,10 @@ ext {
supportLibVersion = '27.1.0'
exoPlayerLibVersion = '2.7.1'
roomDbLibVersion = '1.0.0'
leakCanaryVersion = '1.5.4'
okHttpVersion = '1.5.0'
icepickVersion = '3.2.0'
leakCanaryLibVersion = '1.5.4'
okHttpLibVersion = '1.5.0'
icepickLibVersion = '3.2.0'
stethoLibVersion = '1.5.0'
}
dependencies {
androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2') {
@ -81,8 +82,8 @@ dependencies {
implementation "com.google.android.exoplayer:exoplayer:$exoPlayerLibVersion"
implementation "com.google.android.exoplayer:extension-mediasession:$exoPlayerLibVersion"
debugImplementation "com.facebook.stetho:stetho:$okHttpVersion"
debugImplementation "com.facebook.stetho:stetho-urlconnection:$okHttpVersion"
debugImplementation "com.facebook.stetho:stetho:$stethoLibVersion"
debugImplementation "com.facebook.stetho:stetho-urlconnection:$stethoLibVersion"
debugImplementation 'com.android.support:multidex:1.0.3'
implementation 'io.reactivex.rxjava2:rxjava:2.1.10'
@ -93,13 +94,13 @@ dependencies {
implementation "android.arch.persistence.room:rxjava2:$roomDbLibVersion"
annotationProcessor "android.arch.persistence.room:compiler:$roomDbLibVersion"
implementation "frankiesardo:icepick:$icepickVersion"
annotationProcessor "frankiesardo:icepick-processor:$icepickVersion"
implementation "frankiesardo:icepick:$icepickLibVersion"
annotationProcessor "frankiesardo:icepick-processor:$icepickLibVersion"
debugImplementation "com.squareup.leakcanary:leakcanary-android:$leakCanaryVersion"
betaImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryVersion"
releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryVersion"
debugImplementation "com.squareup.leakcanary:leakcanary-android:$leakCanaryLibVersion"
betaImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryLibVersion"
releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryLibVersion"
implementation 'com.squareup.okhttp3:okhttp:3.9.1'
debugImplementation "com.facebook.stetho:stetho-okhttp3:$okHttpVersion"
debugImplementation "com.facebook.stetho:stetho-okhttp3:$okHttpLibVersion"
}

View File

@ -28,6 +28,12 @@
</intent-filter>
</activity>
<receiver android:name="android.support.v4.media.session.MediaButtonReceiver" >
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
<activity
android:name=".player.old.PlayVideoActivity"
android:configChanges="orientation|keyboardHidden|screenSize"

View File

@ -553,7 +553,7 @@ public abstract class BasePlayer implements
// Ensure dynamic/livestream timeline changes does not cause negative position
if (isPlaylistStable && !isCurrentWindowValid() && !isSynchronizing) {
if (DEBUG) Log.d(TAG, "Playback - negative time position reached, " +
"clamping position to default time.");
"clamping position to 0ms.");
seekTo(/*clampToTime=*/0);
}
break;
@ -639,7 +639,6 @@ public abstract class BasePlayer implements
"[" + getTimeString((int)recoveryPositionMillis) + "]");
seekTo(recoveryPositionMillis);
playQueue.unsetRecovery(currentSourceIndex);
isSynchronizing = false;
} else if (isSynchronizing && simpleExoPlayer.isCurrentWindowDynamic()) {
if (DEBUG) Log.d(TAG, "Playback - Synchronizing livestream to default time");
@ -1111,6 +1110,24 @@ public abstract class BasePlayer implements
return currentItem == null ? context.getString(R.string.unknown_content) : currentItem.getUploader();
}
/** Checks if the current playback is a livestream AND is playing at or beyond the live edge */
public boolean isLiveEdge() {
if (simpleExoPlayer == null) return false;
final boolean isLive = simpleExoPlayer.isCurrentWindowDynamic();
if (!isLive) return false;
final Timeline currentTimeline = simpleExoPlayer.getCurrentTimeline();
final int currentWindowIndex = simpleExoPlayer.getCurrentWindowIndex();
if (currentTimeline.isEmpty() || currentWindowIndex < 0 ||
currentWindowIndex >= currentTimeline.getWindowCount()) {
return false;
}
Timeline.Window timelineWindow = new Timeline.Window();
currentTimeline.getWindow(currentWindowIndex, timelineWindow);
return timelineWindow.getDefaultPositionMs() <= simpleExoPlayer.getCurrentPosition();
}
public boolean isPlaying() {
final int state = simpleExoPlayer.getPlaybackState();
return (state == Player.STATE_READY || state == Player.STATE_BUFFERING)

View File

@ -19,7 +19,6 @@
package org.schabi.newpipe.player;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
@ -33,6 +32,7 @@ import android.preference.PreferenceManager;
import android.provider.Settings;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
import android.util.DisplayMetrics;
@ -49,6 +49,7 @@ import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.Toast;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
import com.google.android.exoplayer2.ui.SubtitleView;
@ -57,6 +58,7 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.playlist.PlayQueue;
import org.schabi.newpipe.playlist.PlayQueueItem;
@ -87,7 +89,8 @@ import static org.schabi.newpipe.util.StateSaver.KEY_SAVED_STATE;
*
* @author mauriciocolli
*/
public final class MainVideoPlayer extends Activity implements StateSaver.WriteRead {
public final class MainVideoPlayer extends AppCompatActivity
implements StateSaver.WriteRead, PlaybackParameterDialog.Callback {
private static final String TAG = ".MainVideoPlayer";
private static final boolean DEBUG = BasePlayer.DEBUG;
@ -340,6 +343,15 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR
}
}
////////////////////////////////////////////////////////////////////////////
// Playback Parameters Listener
////////////////////////////////////////////////////////////////////////////
@Override
public void onPlaybackParameterChanged(float playbackTempo, float playbackPitch) {
if (playerImpl != null) playerImpl.setPlaybackParameters(playbackTempo, playbackPitch);
}
///////////////////////////////////////////////////////////////////////////
@SuppressWarnings({"unused", "WeakerAccess"})
@ -630,6 +642,12 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR
showControlsThenHide();
}
@Override
public void onPlaybackSpeedClicked() {
PlaybackParameterDialog.newInstance(getPlaybackSpeed(), getPlaybackPitch())
.show(getSupportFragmentManager(), TAG);
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
super.onStopTrackingTouch(seekBar);

View File

@ -31,6 +31,7 @@ import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
import org.schabi.newpipe.fragments.local.dialog.PlaylistAppendDialog;
import org.schabi.newpipe.player.event.PlayerEventListener;
import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playlist.PlayQueueItemBuilder;
import org.schabi.newpipe.playlist.PlayQueueItemHolder;
@ -43,7 +44,8 @@ import static org.schabi.newpipe.player.helper.PlayerHelper.formatPitch;
import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed;
public abstract class ServicePlayerActivity extends AppCompatActivity
implements PlayerEventListener, SeekBar.OnSeekBarChangeListener, View.OnClickListener {
implements PlayerEventListener, SeekBar.OnSeekBarChangeListener,
View.OnClickListener, PlaybackParameterDialog.Callback {
private boolean serviceBound;
private ServiceConnection serviceConnection;
@ -57,8 +59,6 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
////////////////////////////////////////////////////////////////////////////
private static final int RECYCLER_ITEM_POPUP_MENU_GROUP_ID = 47;
private static final int PLAYBACK_SPEED_POPUP_MENU_GROUP_ID = 61;
private static final int PLAYBACK_PITCH_POPUP_MENU_GROUP_ID = 97;
private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80;
@ -85,9 +85,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
private ProgressBar progressBar;
private TextView playbackSpeedButton;
private PopupMenu playbackSpeedPopupMenu;
private TextView playbackPitchButton;
private PopupMenu playbackPitchPopupMenu;
////////////////////////////////////////////////////////////////////////////
// Abstracts
@ -317,45 +315,6 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
shuffleButton.setOnClickListener(this);
playbackSpeedButton.setOnClickListener(this);
playbackPitchButton.setOnClickListener(this);
playbackSpeedPopupMenu = new PopupMenu(this, playbackSpeedButton);
playbackPitchPopupMenu = new PopupMenu(this, playbackPitchButton);
buildPlaybackSpeedMenu();
buildPlaybackPitchMenu();
}
private void buildPlaybackSpeedMenu() {
if (playbackSpeedPopupMenu == null) return;
playbackSpeedPopupMenu.getMenu().removeGroup(PLAYBACK_SPEED_POPUP_MENU_GROUP_ID);
for (int i = 0; i < BasePlayer.PLAYBACK_SPEEDS.length; i++) {
final float playbackSpeed = BasePlayer.PLAYBACK_SPEEDS[i];
final String formattedSpeed = formatSpeed(playbackSpeed);
final MenuItem item = playbackSpeedPopupMenu.getMenu().add(PLAYBACK_SPEED_POPUP_MENU_GROUP_ID, i, Menu.NONE, formattedSpeed);
item.setOnMenuItemClickListener(menuItem -> {
if (player == null) return false;
player.setPlaybackSpeed(playbackSpeed);
return true;
});
}
}
private void buildPlaybackPitchMenu() {
if (playbackPitchPopupMenu == null) return;
playbackPitchPopupMenu.getMenu().removeGroup(PLAYBACK_PITCH_POPUP_MENU_GROUP_ID);
for (int i = 0; i < BasePlayer.PLAYBACK_PITCHES.length; i++) {
final float playbackPitch = BasePlayer.PLAYBACK_PITCHES[i];
final String formattedPitch = formatPitch(playbackPitch);
final MenuItem item = playbackPitchPopupMenu.getMenu().add(PLAYBACK_PITCH_POPUP_MENU_GROUP_ID, i, Menu.NONE, formattedPitch);
item.setOnMenuItemClickListener(menuItem -> {
if (player == null) return false;
player.setPlaybackPitch(playbackPitch);
return true;
});
}
}
private void buildItemPopupMenu(final PlayQueueItem item, final View view) {
@ -474,10 +433,12 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
player.onShuffleClicked();
} else if (view.getId() == playbackSpeedButton.getId()) {
playbackSpeedPopupMenu.show();
PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(),
player.getPlaybackPitch()).show(getSupportFragmentManager(), getTag());
} else if (view.getId() == playbackPitchButton.getId()) {
playbackPitchPopupMenu.show();
PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(),
player.getPlaybackPitch()).show(getSupportFragmentManager(), getTag());
} else if (view.getId() == metadata.getId()) {
scrollToSelected();
@ -488,6 +449,15 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
}
}
////////////////////////////////////////////////////////////////////////////
// Playback Parameters Listener
////////////////////////////////////////////////////////////////////////////
@Override
public void onPlaybackParameterChanged(float playbackTempo, float playbackPitch) {
if (player != null) player.setPlaybackParameters(playbackTempo, playbackPitch);
}
////////////////////////////////////////////////////////////////////////////
// Seekbar Listener
////////////////////////////////////////////////////////////////////////////
@ -539,6 +509,10 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
progressSeekBar.setProgress(currentProgress);
progressCurrentTime.setText(Localization.getDurationString(currentProgress / 1000));
}
if (player != null) {
progressLiveSync.setClickable(!player.isLiveEdge());
}
}
@Override

View File

@ -49,6 +49,7 @@ import android.widget.TextView;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MergingMediaSource;
@ -523,6 +524,12 @@ public abstract class VideoPlayer extends BasePlayer
onTextTrackUpdate();
}
@Override
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
super.onPlaybackParametersChanged(playbackParameters);
playbackSpeedTextView.setText(formatSpeed(playbackParameters.speed));
}
@Override
public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {
if (DEBUG) {
@ -615,6 +622,7 @@ public abstract class VideoPlayer extends BasePlayer
if (DEBUG && bufferPercent % 20 == 0) { //Limit log
Log.d(TAG, "updateProgress() called with: isVisible = " + isControlsVisible() + ", currentProgress = [" + currentProgress + "], duration = [" + duration + "], bufferPercent = [" + bufferPercent + "]");
}
playbackLiveSync.setClickable(!isLiveEdge());
}
@Override
@ -718,7 +726,7 @@ public abstract class VideoPlayer extends BasePlayer
wasPlaying = simpleExoPlayer.getPlayWhenReady();
}
private void onPlaybackSpeedClicked() {
public void onPlaybackSpeedClicked() {
if (DEBUG) Log.d(TAG, "onPlaybackSpeedClicked() called");
playbackSpeedPopupMenu.show();
isSomePopupMenuVisible = true;

View File

@ -0,0 +1,348 @@
package org.schabi.newpipe.player.helper;
import android.app.Dialog;
import android.content.Context;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.DialogFragment;
import android.support.v7.app.AlertDialog;
import android.util.Log;
import android.view.View;
import android.widget.CheckBox;
import android.widget.SeekBar;
import android.widget.TextView;
import org.schabi.newpipe.R;
import static org.schabi.newpipe.player.BasePlayer.DEBUG;
public class PlaybackParameterDialog extends DialogFragment {
private static final String TAG = "PlaybackParameterDialog";
public static final float MINIMUM_PLAYBACK_VALUE = 0.25f;
public static final float MAXIMUM_PLAYBACK_VALUE = 3.00f;
public static final String STEP_UP_SIGN = "+";
public static final String STEP_DOWN_SIGN = "-";
public static final float PLAYBACK_STEP_VALUE = 0.05f;
public static final float NIGHTCORE_TEMPO = 1.20f;
public static final float NIGHTCORE_PITCH_LOWER = 1.15f;
public static final float NIGHTCORE_PITCH_UPPER = 1.25f;
public static final float DEFAULT_TEMPO = 1.00f;
public static final float DEFAULT_PITCH = 1.00f;
private static final String INITIAL_TEMPO_KEY = "initial_tempo_key";
private static final String INITIAL_PITCH_KEY = "initial_pitch_key";
public interface Callback {
void onPlaybackParameterChanged(final float playbackTempo, final float playbackPitch);
}
private Callback callback;
private float initialTempo = DEFAULT_TEMPO;
private float initialPitch = DEFAULT_PITCH;
private SeekBar tempoSlider;
private TextView tempoMinimumText;
private TextView tempoMaximumText;
private TextView tempoCurrentText;
private TextView tempoStepDownText;
private TextView tempoStepUpText;
private SeekBar pitchSlider;
private TextView pitchMinimumText;
private TextView pitchMaximumText;
private TextView pitchCurrentText;
private TextView pitchStepDownText;
private TextView pitchStepUpText;
private CheckBox unhookingCheckbox;
private TextView nightCorePresetText;
private TextView resetPresetText;
public static PlaybackParameterDialog newInstance(final float playbackTempo,
final float playbackPitch) {
PlaybackParameterDialog dialog = new PlaybackParameterDialog();
dialog.initialTempo = playbackTempo;
dialog.initialPitch = playbackPitch;
return dialog;
}
/*//////////////////////////////////////////////////////////////////////////
// Lifecycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onAttach(Context context) {
super.onAttach(context);
if (context != null && context instanceof Callback) {
callback = (Callback) context;
} else {
dismiss();
}
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
initialTempo = savedInstanceState.getFloat(INITIAL_TEMPO_KEY, DEFAULT_TEMPO);
initialPitch = savedInstanceState.getFloat(INITIAL_PITCH_KEY, DEFAULT_PITCH);
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putFloat(INITIAL_TEMPO_KEY, initialTempo);
outState.putFloat(INITIAL_PITCH_KEY, initialPitch);
}
/*//////////////////////////////////////////////////////////////////////////
// Dialog
//////////////////////////////////////////////////////////////////////////*/
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
final View view = View.inflate(getContext(), R.layout.dialog_playback_parameter, null);
setupView(view);
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireActivity())
.setTitle(R.string.playback_speed_control)
.setView(view)
.setCancelable(true)
.setNegativeButton(R.string.cancel, (dialogInterface, i) ->
setPlaybackParameters(initialTempo, initialPitch))
.setPositiveButton(R.string.finish, (dialogInterface, i) ->
setPlaybackParameters(getCurrentTempo(), getCurrentPitch()));
return dialogBuilder.create();
}
/*//////////////////////////////////////////////////////////////////////////
// Dialog Builder
//////////////////////////////////////////////////////////////////////////*/
private void setupView(@NonNull View rootView) {
setupHookingControl(rootView);
setupTempoControl(rootView);
setupPitchControl(rootView);
setupPresetControl(rootView);
}
private void setupTempoControl(@NonNull View rootView) {
tempoSlider = rootView.findViewById(R.id.tempoSeekbar);
tempoMinimumText = rootView.findViewById(R.id.tempoMinimumText);
tempoMaximumText = rootView.findViewById(R.id.tempoMaximumText);
tempoCurrentText = rootView.findViewById(R.id.tempoCurrentText);
tempoStepUpText = rootView.findViewById(R.id.tempoStepUp);
tempoStepDownText = rootView.findViewById(R.id.tempoStepDown);
tempoCurrentText.setText(PlayerHelper.formatSpeed(initialTempo));
tempoMaximumText.setText(PlayerHelper.formatSpeed(MAXIMUM_PLAYBACK_VALUE));
tempoMinimumText.setText(PlayerHelper.formatSpeed(MINIMUM_PLAYBACK_VALUE));
tempoStepUpText.setText(getStepUpPercentString(PLAYBACK_STEP_VALUE));
tempoStepUpText.setOnClickListener(view ->
setTempo(getCurrentTempo() + PLAYBACK_STEP_VALUE));
tempoStepDownText.setText(getStepDownPercentString(PLAYBACK_STEP_VALUE));
tempoStepDownText.setOnClickListener(view ->
setTempo(getCurrentTempo() - PLAYBACK_STEP_VALUE));
tempoSlider.setMax(getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, MAXIMUM_PLAYBACK_VALUE));
tempoSlider.setProgress(getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, initialTempo));
tempoSlider.setOnSeekBarChangeListener(getOnTempoChangedListener());
}
private SeekBar.OnSeekBarChangeListener getOnTempoChangedListener() {
return new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
final float currentTempo = getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, progress);
if (fromUser) { // this change is first in chain
setTempo(currentTempo);
} else {
setPlaybackParameters(currentTempo, getCurrentPitch());
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
// Do Nothing.
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
// Do Nothing.
}
};
}
private void setupPitchControl(@NonNull View rootView) {
pitchSlider = rootView.findViewById(R.id.pitchSeekbar);
pitchMinimumText = rootView.findViewById(R.id.pitchMinimumText);
pitchMaximumText = rootView.findViewById(R.id.pitchMaximumText);
pitchCurrentText = rootView.findViewById(R.id.pitchCurrentText);
pitchStepDownText = rootView.findViewById(R.id.pitchStepDown);
pitchStepUpText = rootView.findViewById(R.id.pitchStepUp);
pitchCurrentText.setText(PlayerHelper.formatPitch(initialPitch));
pitchMaximumText.setText(PlayerHelper.formatPitch(MAXIMUM_PLAYBACK_VALUE));
pitchMinimumText.setText(PlayerHelper.formatPitch(MINIMUM_PLAYBACK_VALUE));
pitchStepUpText.setText(getStepUpPercentString(PLAYBACK_STEP_VALUE));
pitchStepUpText.setOnClickListener(view ->
setPitch(getCurrentPitch() + PLAYBACK_STEP_VALUE));
pitchStepDownText.setText(getStepDownPercentString(PLAYBACK_STEP_VALUE));
pitchStepDownText.setOnClickListener(view ->
setPitch(getCurrentPitch() - PLAYBACK_STEP_VALUE));
pitchSlider.setMax(getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, MAXIMUM_PLAYBACK_VALUE));
pitchSlider.setProgress(getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, initialPitch));
pitchSlider.setOnSeekBarChangeListener(getOnPitchChangedListener());
}
private SeekBar.OnSeekBarChangeListener getOnPitchChangedListener() {
return new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
final float currentPitch = getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, progress);
if (fromUser) { // this change is first in chain
setPitch(currentPitch);
} else {
setPlaybackParameters(getCurrentTempo(), currentPitch);
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
// Do Nothing.
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
// Do Nothing.
}
};
}
private void setupHookingControl(@NonNull View rootView) {
unhookingCheckbox = rootView.findViewById(R.id.unhookCheckbox);
unhookingCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> {
if (isChecked) return;
// When unchecked, slide back to the minimum of current tempo or pitch
final float minimum = Math.min(getCurrentPitch(), getCurrentTempo());
setSliders(minimum);
});
}
private void setupPresetControl(@NonNull View rootView) {
nightCorePresetText = rootView.findViewById(R.id.presetNightcore);
nightCorePresetText.setOnClickListener(view -> {
final float randomPitch = NIGHTCORE_PITCH_LOWER +
(float) Math.random() * (NIGHTCORE_PITCH_UPPER - NIGHTCORE_PITCH_LOWER);
setTempoSlider(NIGHTCORE_TEMPO);
setPitchSlider(randomPitch);
});
resetPresetText = rootView.findViewById(R.id.presetReset);
resetPresetText.setOnClickListener(view -> {
setTempoSlider(DEFAULT_TEMPO);
setPitchSlider(DEFAULT_PITCH);
});
}
/*//////////////////////////////////////////////////////////////////////////
// Helper
//////////////////////////////////////////////////////////////////////////*/
private void setTempo(final float newTempo) {
if (unhookingCheckbox == null) return;
if (!unhookingCheckbox.isChecked()) {
setSliders(newTempo);
} else {
setTempoSlider(newTempo);
}
}
private void setPitch(final float newPitch) {
if (unhookingCheckbox == null) return;
if (!unhookingCheckbox.isChecked()) {
setSliders(newPitch);
} else {
setPitchSlider(newPitch);
}
}
private void setSliders(final float newValue) {
setTempoSlider(newValue);
setPitchSlider(newValue);
}
private void setTempoSlider(final float newTempo) {
if (tempoSlider == null) return;
// seekbar doesn't register progress if it is the same as the existing progress
tempoSlider.setProgress(Integer.MAX_VALUE);
tempoSlider.setProgress(getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, newTempo));
}
private void setPitchSlider(final float newPitch) {
if (pitchSlider == null) return;
pitchSlider.setProgress(Integer.MAX_VALUE);
pitchSlider.setProgress(getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, newPitch));
}
private void setPlaybackParameters(final float tempo, final float pitch) {
if (callback != null && tempoCurrentText != null && pitchCurrentText != null) {
if (DEBUG) Log.d(TAG, "Setting playback parameters to " +
"tempo=[" + tempo + "], " +
"pitch=[" + pitch + "]");
tempoCurrentText.setText(PlayerHelper.formatSpeed(tempo));
pitchCurrentText.setText(PlayerHelper.formatPitch(pitch));
callback.onPlaybackParameterChanged(tempo, pitch);
}
}
private float getCurrentTempo() {
return tempoSlider == null ? initialTempo : getSliderEquivalent(MINIMUM_PLAYBACK_VALUE,
tempoSlider.getProgress());
}
private float getCurrentPitch() {
return pitchSlider == null ? initialPitch : getSliderEquivalent(MINIMUM_PLAYBACK_VALUE,
pitchSlider.getProgress());
}
/**
* Converts from zeroed float with a minimum offset to the nearest rounded slider
* equivalent integer
* */
private static int getSliderEquivalent(final float minimumValue, final float floatValue) {
return Math.round((floatValue - minimumValue) * 100f);
}
/**
* Converts from slider integer value to an equivalent float value with a given minimum offset
* */
private static float getSliderEquivalent(final float minimumValue, final int intValue) {
return ((float) intValue) / 100f + minimumValue;
}
private static String getStepUpPercentString(final float percent) {
return STEP_UP_SIGN + PlayerHelper.formatPitch(percent);
}
private static String getStepDownPercentString(final float percent) {
return STEP_DOWN_SIGN + PlayerHelper.formatPitch(percent);
}
}

View File

@ -0,0 +1,313 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="false"
android:paddingLeft="@dimen/video_item_search_padding"
android:paddingRight="@dimen/video_item_search_padding"
android:paddingTop="@dimen/video_item_search_padding">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="vertical"
android:scrollbarAlwaysDrawVerticalTrack="true">
<!-- START HERE -->
<TextView
android:id="@+id/tempoControlText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_centerHorizontal="true"
android:text="@string/playback_tempo"
android:textStyle="bold"
android:textColor="?attr/colorAccent"
android:layout_alignParentTop="true"/>
<RelativeLayout
android:id="@+id/tempoControl"
android:layout_width="match_parent"
android:layout_height="40dp"
android:orientation="horizontal"
android:layout_marginTop="4dp"
android:layout_below="@id/tempoControlText">
<TextView
android:id="@+id/tempoStepDown"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:layout_centerVertical="true"
android:clickable="true"
android:focusable="true"
android:text="--%"
android:textStyle="bold"
android:textColor="?attr/colorAccent"
android:background="?attr/selectableItemBackground"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
tools:ignore="HardcodedText"
tools:text="-5%"/>
<RelativeLayout
android:id="@+id/tempoDisplay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
android:layout_toRightOf="@id/tempoStepDown"
android:layout_toEndOf="@id/tempoStepDown"
android:layout_toLeftOf="@id/tempoStepUp"
android:layout_toStartOf="@id/tempoStepUp">
<TextView
android:id="@+id/tempoMinimumText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="-.--x"
android:textColor="?attr/colorAccent"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_marginLeft="4dp"
android:layout_marginStart="4dp"
tools:ignore="HardcodedText"
tools:text="1.00x"/>
<TextView
android:id="@+id/tempoCurrentText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="---%"
android:textColor="?attr/colorAccent"
android:layout_centerHorizontal="true"
android:textStyle="bold"
tools:ignore="HardcodedText"
tools:text="100%"/>
<TextView
android:id="@+id/tempoMaximumText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="---%"
android:textColor="?attr/colorAccent"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_marginRight="4dp"
android:layout_marginEnd="4dp"
tools:ignore="HardcodedText"
tools:text="300%"/>
<android.support.v7.widget.AppCompatSeekBar
android:id="@+id/tempoSeekbar"
style="@style/Widget.AppCompat.SeekBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/tempoCurrentText"
android:paddingBottom="4dp"
tools:progress="50"/>
</RelativeLayout>
<TextView
android:id="@+id/tempoStepUp"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:clickable="true"
android:focusable="true"
android:text="+-%"
android:textStyle="bold"
android:textColor="?attr/colorAccent"
android:background="?attr/selectableItemBackground"
android:layout_centerVertical="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_marginRight="4dp"
android:layout_marginEnd="4dp"
tools:ignore="HardcodedText"
tools:text="+5%"/>
</RelativeLayout>
<View
android:id="@+id/separatorPitch"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_below="@id/tempoControl"
android:layout_margin="@dimen/video_item_search_padding"
android:background="?attr/separator_color"/>
<TextView
android:id="@+id/pitchControlText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_centerHorizontal="true"
android:text="@string/playback_pitch"
android:textStyle="bold"
android:textColor="?attr/colorAccent"
android:layout_below="@id/separatorPitch"/>
<RelativeLayout
android:id="@+id/pitchControl"
android:layout_width="match_parent"
android:layout_height="40dp"
android:orientation="horizontal"
android:layout_marginTop="4dp"
android:layout_below="@id/pitchControlText">
<TextView
android:id="@+id/pitchStepDown"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:layout_centerVertical="true"
android:clickable="true"
android:focusable="true"
android:text="--%"
android:textStyle="bold"
android:textColor="?attr/colorAccent"
android:background="?attr/selectableItemBackground"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
tools:ignore="HardcodedText"
tools:text="-5%"/>
<RelativeLayout
android:id="@+id/pitchDisplay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
android:layout_toRightOf="@+id/pitchStepDown"
android:layout_toEndOf="@+id/pitchStepDown"
android:layout_toLeftOf="@+id/pitchStepUp"
android:layout_toStartOf="@+id/pitchStepUp">
<TextView
android:id="@+id/pitchMinimumText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="---%"
android:textColor="?attr/colorAccent"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_marginLeft="4dp"
android:layout_marginStart="4dp"
tools:ignore="HardcodedText"
tools:text="25%"/>
<TextView
android:id="@+id/pitchCurrentText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="---%"
android:textColor="?attr/colorAccent"
android:layout_centerHorizontal="true"
android:textStyle="bold"
tools:ignore="HardcodedText"
tools:text="100%"/>
<TextView
android:id="@+id/pitchMaximumText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="---%"
android:textColor="?attr/colorAccent"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_marginRight="4dp"
android:layout_marginEnd="4dp"
tools:ignore="HardcodedText"
tools:text="300%"/>
<android.support.v7.widget.AppCompatSeekBar
android:id="@+id/pitchSeekbar"
style="@style/Widget.AppCompat.SeekBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/pitchCurrentText"
android:paddingBottom="4dp"
tools:progress="50"/>
</RelativeLayout>
<TextView
android:id="@+id/pitchStepUp"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:clickable="true"
android:focusable="true"
android:text="+-%"
android:textStyle="bold"
android:textColor="?attr/colorAccent"
android:background="?attr/selectableItemBackground"
android:layout_centerVertical="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_marginRight="4dp"
android:layout_marginEnd="4dp"
tools:ignore="HardcodedText"
tools:text="+5%"/>
</RelativeLayout>
<View
android:id="@+id/separatorCheckbox"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_below="@+id/pitchControl"
android:layout_margin="@dimen/video_item_search_padding"
android:background="?attr/separator_color"/>
<CheckBox
android:id="@+id/unhookCheckbox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="false"
android:clickable="true"
android:focusable="true"
android:text="@string/unhook_checkbox"
android:maxLines="1"
android:layout_centerHorizontal="true"
android:layout_below="@id/separatorCheckbox"/>
<LinearLayout
android:id="@+id/presetSelector"
android:layout_width="match_parent"
android:layout_height="40dp"
android:orientation="horizontal"
android:layout_below="@id/unhookCheckbox">
<TextView
android:id="@+id/presetNightcore"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:text="@string/playback_nightcore"
android:background="?attr/selectableItemBackground"
android:textColor="?attr/colorAccent"/>
<TextView
android:id="@+id/presetReset"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:text="@string/playback_default"
android:background="?attr/selectableItemBackground"
android:textColor="?attr/colorAccent"/>
</LinearLayout>
<!-- END HERE -->
</RelativeLayout>
</ScrollView>

View File

@ -456,4 +456,12 @@
<string name="import_soundcloud_instructions_hint">yourid, soundcloud.com/yourid</string>
<string name="import_network_expensive_warning">Keep in mind that this operation can be network expensive.\n\nDo you want to continue?</string>
<!-- Playback Parameters -->
<string name="playback_speed_control">Playback Speed Control</string>
<string name="playback_tempo">Tempo</string>
<string name="playback_pitch">Pitch</string>
<string name="unhook_checkbox">Unhook (may cause distortion)</string>
<string name="playback_nightcore">Nightcore</string>
<string name="playback_default">Default</string>
</resources>