Merge branch 'dev' into pr3178

This commit is contained in:
Stypox 2020-08-02 22:59:43 +02:00
commit 4dd47f8adf
130 changed files with 7247 additions and 7180 deletions

View file

@ -1,592 +0,0 @@
/*
* Copyright 2017 Mauricio Colli <mauriciocolli@outlook.com>
* BackgroundPlayer.java is part of NewPipe
*
* License: GPL-3.0+
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.schabi.newpipe.player;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.os.Build;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.source.MediaSource;
import com.nostra13.universalimageloader.core.assist.FailReason;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.player.event.PlayerEventListener;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.player.resolver.AudioPlaybackResolver;
import org.schabi.newpipe.player.resolver.MediaSourceTag;
import org.schabi.newpipe.util.ThemeHelper;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
/**
* Service Background Player implementing {@link VideoPlayer}.
*
* @author mauriciocolli
*/
public final class BackgroundPlayer extends Service {
private static final String TAG = "BackgroundPlayer";
private static final boolean DEBUG = BasePlayer.DEBUG;
public static final String ACTION_CLOSE
= "org.schabi.newpipe.player.BackgroundPlayer.CLOSE";
public static final String ACTION_PLAY_PAUSE
= "org.schabi.newpipe.player.BackgroundPlayer.PLAY_PAUSE";
public static final String ACTION_REPEAT
= "org.schabi.newpipe.player.BackgroundPlayer.REPEAT";
public static final String ACTION_PLAY_NEXT
= "org.schabi.newpipe.player.BackgroundPlayer.ACTION_PLAY_NEXT";
public static final String ACTION_PLAY_PREVIOUS
= "org.schabi.newpipe.player.BackgroundPlayer.ACTION_PLAY_PREVIOUS";
public static final String ACTION_FAST_REWIND
= "org.schabi.newpipe.player.BackgroundPlayer.ACTION_FAST_REWIND";
public static final String ACTION_FAST_FORWARD
= "org.schabi.newpipe.player.BackgroundPlayer.ACTION_FAST_FORWARD";
public static final String ACTION_BUFFERING
= "org.schabi.newpipe.player.BackgroundPlayer.ACTION_BUFFERING";
public static final String ACTION_SHUFFLE
= "org.schabi.newpipe.player.BackgroundPlayer.ACTION_SHUFFLE";
public static final String SET_IMAGE_RESOURCE_METHOD = "setImageResource";
private BasePlayerImpl basePlayerImpl;
private SharedPreferences sharedPreferences;
private boolean shouldUpdateOnProgress; // only used for old notifications
private boolean isForwardPressed;
private boolean isRewindPressed;
/*//////////////////////////////////////////////////////////////////////////
// Service-Activity Binder
//////////////////////////////////////////////////////////////////////////*/
private PlayerEventListener activityListener;
private IBinder mBinder;
/*//////////////////////////////////////////////////////////////////////////
// Service's LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreate() {
if (DEBUG) {
Log.d(TAG, "onCreate() called");
}
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
assureCorrectAppLanguage(this);
ThemeHelper.setTheme(this);
basePlayerImpl = new BasePlayerImpl(this);
basePlayerImpl.setup();
mBinder = new PlayerServiceBinder(basePlayerImpl);
shouldUpdateOnProgress = true;
}
@Override
public int onStartCommand(final Intent intent, final int flags, final int startId) {
if (DEBUG) {
Log.d(TAG, "N_ onStartCommand() called with: intent = [" + intent + "], "
+ "flags = [" + flags + "], startId = [" + startId + "]");
}
basePlayerImpl.handleIntent(intent);
if (basePlayerImpl.mediaSessionManager != null) {
basePlayerImpl.mediaSessionManager.handleMediaButtonIntent(intent);
}
return START_NOT_STICKY;
}
@Override
public void onDestroy() {
if (DEBUG) {
Log.d(TAG, "destroy() called");
}
onClose();
}
@Override
protected void attachBaseContext(final Context base) {
super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base));
}
@Override
public IBinder onBind(final Intent intent) {
return mBinder;
}
/*//////////////////////////////////////////////////////////////////////////
// Actions
//////////////////////////////////////////////////////////////////////////*/
private void onClose() {
if (DEBUG) {
Log.d(TAG, "N_ onClose() called");
}
if (basePlayerImpl != null) {
basePlayerImpl.savePlaybackState();
basePlayerImpl.stopActivityBinding();
basePlayerImpl.destroy();
}
NotificationUtil.getInstance()
.cancelNotification(NotificationUtil.NOTIFICATION_ID_BACKGROUND);
mBinder = null;
basePlayerImpl = null;
stopForeground(true);
stopSelf();
}
private void onScreenOnOff(final boolean on) {
if (DEBUG) {
Log.d(TAG, "onScreenOnOff() called with: on = [" + on + "]");
}
shouldUpdateOnProgress = on;
basePlayerImpl.triggerProgressUpdate();
if (on) {
basePlayerImpl.startProgressLoop();
} else {
basePlayerImpl.stopProgressLoop();
}
}
protected class BasePlayerImpl extends BasePlayer {
@NonNull
private final AudioPlaybackResolver resolver;
BasePlayerImpl(final Context context) {
super(context);
this.resolver = new AudioPlaybackResolver(context, dataSource);
}
@Override
public void initPlayer(final boolean playOnReady) {
super.initPlayer(playOnReady);
}
@Override
public void handleIntent(final Intent intent) {
super.handleIntent(intent);
if (DEBUG) {
Log.d(TAG, "N_ handleIntent()");
}
NotificationUtil.getInstance().recreateBackgroundPlayerNotification(context,
basePlayerImpl.mediaSessionManager.getSessionToken(), basePlayerImpl,
sharedPreferences, true); // false
NotificationUtil.getInstance().setProgressbarOnOldNotifications(100, 0, false);
startForeground(NotificationUtil.NOTIFICATION_ID_BACKGROUND,
NotificationUtil.getInstance().notificationBuilder.build());
}
/*//////////////////////////////////////////////////////////////////////////
// Thumbnail Loading
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onLoadingComplete(final String imageUri, final View view,
final Bitmap loadedImage) {
super.onLoadingComplete(imageUri, view, loadedImage);
if (DEBUG) {
Log.d(TAG, "N_ onLoadingComplete()");
}
NotificationUtil.getInstance().recreateBackgroundPlayerNotification(context,
basePlayerImpl.mediaSessionManager.getSessionToken(), basePlayerImpl,
sharedPreferences, true); //true
NotificationUtil.getInstance().updateOldNotificationsThumbnail(basePlayerImpl);
NotificationUtil.getInstance().updateBackgroundPlayerNotification(-1,
getBaseContext(), basePlayerImpl, sharedPreferences);
}
@Override
public void onLoadingFailed(final String imageUri, final View view,
final FailReason failReason) {
super.onLoadingFailed(imageUri, view, failReason);
if (DEBUG) {
Log.d(TAG, "N_ onLoadingFailed()");
}
NotificationUtil.getInstance().recreateBackgroundPlayerNotification(context,
basePlayerImpl.mediaSessionManager.getSessionToken(), basePlayerImpl,
sharedPreferences, true); //true
NotificationUtil.getInstance().updateOldNotificationsThumbnail(basePlayerImpl);
NotificationUtil.getInstance().updateBackgroundPlayerNotification(-1,
getBaseContext(), basePlayerImpl, sharedPreferences);
}
/*//////////////////////////////////////////////////////////////////////////
// States Implementation
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onPrepared(final boolean playWhenReady) {
super.onPrepared(playWhenReady);
}
@Override
public void onShuffleClicked() {
super.onShuffleClicked();
if (DEBUG) {
Log.d(TAG, "N_ onShuffleClicked:");
}
NotificationUtil.getInstance().recreateBackgroundPlayerNotification(context,
basePlayerImpl.mediaSessionManager.getSessionToken(), basePlayerImpl,
sharedPreferences);
NotificationUtil.getInstance().updateBackgroundPlayerNotification(-1,
getBaseContext(), basePlayerImpl, sharedPreferences);
updatePlayback();
}
@Override
public void onMuteUnmuteButtonClicked() {
super.onMuteUnmuteButtonClicked();
updatePlayback();
}
@Override
public void onUpdateProgress(final int currentProgress, final int duration,
final int bufferPercent) {
updateProgress(currentProgress, duration, bufferPercent);
// setMetadata only updates the metadata when any of the metadata keys are null
basePlayerImpl.mediaSessionManager.setMetadata(basePlayerImpl.getVideoTitle(),
basePlayerImpl.getUploaderName(), basePlayerImpl.getThumbnail(), duration);
boolean areOldNotificationsEnabled = sharedPreferences.getBoolean(
getString(R.string.enable_old_notifications_key), false);
if (areOldNotificationsEnabled) {
if (!shouldUpdateOnProgress) {
return;
}
if (NotificationUtil.timesNotificationUpdated
> NotificationUtil.NOTIFICATION_UPDATES_BEFORE_RESET) {
NotificationUtil.getInstance().recreateBackgroundPlayerNotification(context,
basePlayerImpl.mediaSessionManager.getSessionToken(), basePlayerImpl,
sharedPreferences);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationUtil.getInstance()
.updateOldNotificationsThumbnail(basePlayerImpl);
}
}
NotificationUtil.getInstance().setCachedDuration(currentProgress, duration);
NotificationUtil.getInstance().setProgressbarOnOldNotifications(duration,
currentProgress, false);
NotificationUtil.getInstance().updateBackgroundPlayerNotification(-1,
getBaseContext(), basePlayerImpl, sharedPreferences);
}
}
@Override
public void onPlayPrevious() {
super.onPlayPrevious();
triggerProgressUpdate();
}
@Override
public void onPlayNext() {
super.onPlayNext();
triggerProgressUpdate();
}
@Override
public void destroy() {
super.destroy();
NotificationUtil.getInstance().unsetImageInOldNotifications();
}
/*//////////////////////////////////////////////////////////////////////////
// ExoPlayer Listener
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onPlaybackParametersChanged(final PlaybackParameters playbackParameters) {
super.onPlaybackParametersChanged(playbackParameters);
updatePlayback();
}
@Override
public void onLoadingChanged(final boolean isLoading) {
// Disable default behavior
}
@Override
public void onRepeatModeChanged(final int i) {
if (DEBUG) {
Log.d(TAG, "N_ onRepeatModeChanged()");
}
NotificationUtil.getInstance().recreateBackgroundPlayerNotification(context,
basePlayerImpl.mediaSessionManager.getSessionToken(), basePlayerImpl,
sharedPreferences);
NotificationUtil.getInstance().updateBackgroundPlayerNotification(-1,
getBaseContext(), basePlayerImpl, sharedPreferences);
updatePlayback();
}
/*//////////////////////////////////////////////////////////////////////////
// Playback Listener
//////////////////////////////////////////////////////////////////////////*/
protected void onMetadataChanged(@NonNull final MediaSourceTag tag) {
super.onMetadataChanged(tag);
if (DEBUG) {
Log.d(TAG, "N_ onMetadataChanged()");
}
NotificationUtil.getInstance().recreateBackgroundPlayerNotification(context,
basePlayerImpl.mediaSessionManager.getSessionToken(), basePlayerImpl,
sharedPreferences);
NotificationUtil.getInstance().updateOldNotificationsThumbnail(basePlayerImpl);
NotificationUtil.getInstance().updateBackgroundPlayerNotification(-1,
getBaseContext(), basePlayerImpl, sharedPreferences);
updateMetadata();
}
@Override
@Nullable
public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) {
return resolver.resolve(info);
}
@Override
public void onPlaybackShutdown() {
super.onPlaybackShutdown();
onClose();
}
/*//////////////////////////////////////////////////////////////////////////
// Activity Event Listener
//////////////////////////////////////////////////////////////////////////*/
/*package-private*/ void setActivityListener(final PlayerEventListener listener) {
activityListener = listener;
updateMetadata();
updatePlayback();
triggerProgressUpdate();
}
/*package-private*/ void removeActivityListener(final PlayerEventListener listener) {
if (activityListener == listener) {
activityListener = null;
}
}
private void updateMetadata() {
if (activityListener != null && getCurrentMetadata() != null) {
activityListener.onMetadataUpdate(getCurrentMetadata().getMetadata());
}
}
private void updatePlayback() {
if (activityListener != null && simpleExoPlayer != null && playQueue != null) {
activityListener.onPlaybackUpdate(currentState, getRepeatMode(),
playQueue.isShuffled(), getPlaybackParameters());
}
}
private void updateProgress(final int currentProgress, final int duration,
final int bufferPercent) {
if (activityListener != null) {
activityListener.onProgressUpdate(currentProgress, duration, bufferPercent);
}
}
private void stopActivityBinding() {
if (activityListener != null) {
activityListener.onServiceStopped();
activityListener = null;
}
}
/*//////////////////////////////////////////////////////////////////////////
// Broadcast Receiver
//////////////////////////////////////////////////////////////////////////*/
@Override
protected void setupBroadcastReceiver(final IntentFilter intentFilter) {
super.setupBroadcastReceiver(intentFilter);
intentFilter.addAction(ACTION_CLOSE);
intentFilter.addAction(ACTION_PLAY_PAUSE);
intentFilter.addAction(ACTION_REPEAT);
intentFilter.addAction(ACTION_PLAY_PREVIOUS);
intentFilter.addAction(ACTION_PLAY_NEXT);
intentFilter.addAction(ACTION_FAST_REWIND);
intentFilter.addAction(ACTION_FAST_FORWARD);
intentFilter.addAction(ACTION_BUFFERING);
intentFilter.addAction(ACTION_SHUFFLE);
intentFilter.addAction(Intent.ACTION_SCREEN_ON);
intentFilter.addAction(Intent.ACTION_SCREEN_OFF);
intentFilter.addAction(Intent.ACTION_HEADSET_PLUG);
}
@Override
public void onBroadcastReceived(final Intent intent) {
super.onBroadcastReceived(intent);
if (intent == null || intent.getAction() == null) {
return;
}
if (DEBUG) {
// FIXME remove N_
Log.d(TAG, "N_ onBroadcastReceived() called with: intent = [" + intent + "]");
}
switch (intent.getAction()) {
case ACTION_CLOSE:
onClose();
break;
case ACTION_PLAY_PAUSE:
onPlayPause();
break;
case ACTION_REPEAT:
onRepeatClicked();
break;
case ACTION_PLAY_NEXT:
onPlayNext();
break;
case ACTION_PLAY_PREVIOUS:
onPlayPrevious();
break;
case ACTION_FAST_FORWARD:
isForwardPressed = true;
onFastForward();
break;
case ACTION_FAST_REWIND:
isRewindPressed = true;
onFastRewind();
break;
case Intent.ACTION_SCREEN_ON:
onScreenOnOff(true);
break;
case Intent.ACTION_SCREEN_OFF:
onScreenOnOff(false);
break;
case ACTION_BUFFERING:
onBuffering();
break;
case ACTION_SHUFFLE:
onShuffleClicked();
break;
case "android.intent.action.HEADSET_PLUG": //FIXME
/*notificationManager.cancel(NOTIFICATION_ID);
mediaSessionManager.dispose();
mediaSessionManager.enable(getBaseContext(), basePlayerImpl.simpleExoPlayer);*/
break;
}
}
/*//////////////////////////////////////////////////////////////////////////
// States
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onBuffering() {
super.onBuffering();
if (NotificationUtil.getInstance().notificationSlot0.contains("buffering")
|| NotificationUtil.getInstance().notificationSlot1.contains("buffering")
|| NotificationUtil.getInstance().notificationSlot2.contains("buffering")
|| NotificationUtil.getInstance().notificationSlot3.contains("buffering")
|| NotificationUtil.getInstance().notificationSlot4.contains("buffering")) {
if (basePlayerImpl.getCurrentState() == BasePlayer.STATE_PREFLIGHT
|| basePlayerImpl.getCurrentState() == BasePlayer.STATE_BLOCKED
|| basePlayerImpl.getCurrentState() == BasePlayer.STATE_BUFFERING) {
if (!(isForwardPressed || isRewindPressed)) {
if (DEBUG) {
Log.d(TAG, "N_ onBuffering()");
}
NotificationUtil.getInstance().updateBackgroundPlayerNotification(-1,
getBaseContext(), basePlayerImpl, sharedPreferences);
} else {
isForwardPressed = false;
isRewindPressed = false;
}
}
}
}
@Override
public void changeState(final int state) {
super.changeState(state);
updatePlayback();
}
@Override
public void onPlaying() {
super.onPlaying();
if (DEBUG) {
Log.d(TAG, "N_ onPlaying()");
}
NotificationUtil.getInstance().recreateBackgroundPlayerNotification(context,
basePlayerImpl.mediaSessionManager.getSessionToken(), basePlayerImpl,
sharedPreferences);
NotificationUtil.getInstance().updateOldNotificationsThumbnail(basePlayerImpl);
NotificationUtil.getInstance()
.updateBackgroundPlayerNotification(R.drawable.ic_pause_white_24dp,
getBaseContext(), basePlayerImpl, sharedPreferences);
}
@Override
public void onPaused() {
super.onPaused();
if (DEBUG) {
Log.d(TAG, "N_ onPaused()");
}
NotificationUtil.getInstance().recreateBackgroundPlayerNotification(context,
basePlayerImpl.mediaSessionManager.getSessionToken(), basePlayerImpl,
sharedPreferences);
NotificationUtil.getInstance().updateOldNotificationsThumbnail(basePlayerImpl);
NotificationUtil.getInstance()
.updateBackgroundPlayerNotification(R.drawable.ic_play_arrow_white_24dp,
getBaseContext(), basePlayerImpl, sharedPreferences);
}
@Override
public void onCompleted() {
super.onCompleted();
if (DEBUG) {
Log.d(TAG, "N_ onCompleted()");
}
NotificationUtil.getInstance().recreateBackgroundPlayerNotification(context,
basePlayerImpl.mediaSessionManager.getSessionToken(), basePlayerImpl,
sharedPreferences);
NotificationUtil.getInstance().setProgressbarOnOldNotifications(100, 100, false);
NotificationUtil.getInstance().updateOldNotificationsThumbnail(basePlayerImpl);
NotificationUtil.getInstance()
.updateBackgroundPlayerNotification(R.drawable.ic_replay_white_24dp,
getBaseContext(), basePlayerImpl, sharedPreferences);
}
}
}

View file

@ -1,13 +1,13 @@
package org.schabi.newpipe.player;
import android.content.Intent;
import android.view.Menu;
import android.view.MenuItem;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PermissionHelper;
import static org.schabi.newpipe.player.BackgroundPlayer.ACTION_CLOSE;
public final class BackgroundPlayerActivity extends ServicePlayerActivity {
private static final String TAG = "BackgroundPlayerActivity";
@ -19,25 +19,25 @@ public final class BackgroundPlayerActivity extends ServicePlayerActivity {
@Override
public String getSupportActionTitle() {
return getResources().getString(R.string.title_activity_background_player);
return getResources().getString(R.string.title_activity_play_queue);
}
@Override
public Intent getBindIntent() {
return new Intent(this, BackgroundPlayer.class);
return new Intent(this, MainPlayer.class);
}
@Override
public void startPlayerListener() {
if (player != null && player instanceof BackgroundPlayer.BasePlayerImpl) {
((BackgroundPlayer.BasePlayerImpl) player).setActivityListener(this);
if (player instanceof VideoPlayerImpl) {
((VideoPlayerImpl) player).setActivityListener(this);
}
}
@Override
public void stopPlayerListener() {
if (player != null && player instanceof BackgroundPlayer.BasePlayerImpl) {
((BackgroundPlayer.BasePlayerImpl) player).removeActivityListener(this);
if (player instanceof VideoPlayerImpl) {
((VideoPlayerImpl) player).removeActivityListener(this);
}
}
@ -56,18 +56,30 @@ public final class BackgroundPlayerActivity extends ServicePlayerActivity {
}
this.player.setRecovery();
getApplicationContext().sendBroadcast(getPlayerShutdownIntent());
getApplicationContext().startService(
getSwitchIntent(PopupVideoPlayer.class)
.putExtra(BasePlayer.START_PAUSED, !this.player.isPlaying())
);
NavigationHelper.playOnPopupPlayer(
getApplicationContext(), player.playQueue, this.player.isPlaying());
return true;
}
if (item.getItemId() == R.id.action_switch_background) {
this.player.setRecovery();
NavigationHelper.playOnBackgroundPlayer(
getApplicationContext(), player.playQueue, this.player.isPlaying());
return true;
}
return false;
}
@Override
public Intent getPlayerShutdownIntent() {
return new Intent(ACTION_CLOSE);
public void setupMenu(final Menu menu) {
if (player == null) {
return;
}
menu.findItem(R.id.action_switch_popup)
.setVisible(!((VideoPlayerImpl) player).popupPlayerSelected());
menu.findItem(R.id.action_switch_background)
.setVisible(!((VideoPlayerImpl) player).audioPlayerSelected());
}
}

View file

@ -54,6 +54,7 @@ import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.assist.FailReason;
import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
import io.reactivex.android.schedulers.AndroidSchedulers;
import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.DownloaderImpl;
import org.schabi.newpipe.R;
@ -128,13 +129,15 @@ public abstract class BasePlayer implements
@NonNull
public static final String SELECT_ON_APPEND = "select_on_append";
@NonNull
public static final String PLAYER_TYPE = "player_type";
@NonNull
public static final String IS_MUTED = "is_muted";
/*//////////////////////////////////////////////////////////////////////////
// Playback
//////////////////////////////////////////////////////////////////////////*/
protected static final float[] PLAYBACK_SPEEDS = {0.5f, 0.75f, 1f, 1.25f, 1.5f, 1.75f, 2f};
protected static final float[] PLAYBACK_SPEEDS = {0.5f, 0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f};
protected PlayQueue playQueue;
protected PlayQueueAdapter playQueueAdapter;
@ -159,6 +162,10 @@ public abstract class BasePlayer implements
protected static final int PLAY_PREV_ACTIVATION_LIMIT_MILLIS = 5000; // 5 seconds
protected static final int PROGRESS_LOOP_INTERVAL_MILLIS = 500;
public static final int PLAYER_TYPE_VIDEO = 0;
public static final int PLAYER_TYPE_AUDIO = 1;
public static final int PLAYER_TYPE_POPUP = 2;
protected SimpleExoPlayer simpleExoPlayer;
protected AudioReactor audioReactor;
protected MediaSessionManager mediaSessionManager;
@ -173,6 +180,8 @@ public abstract class BasePlayer implements
@NonNull
protected final HistoryRecordManager recordManager;
@NonNull
protected final SharedPreferences sharedPreferences;
@NonNull
protected final CustomTrackSelector trackSelector;
@NonNull
protected final PlayerDataSource dataSource;
@ -204,6 +213,7 @@ public abstract class BasePlayer implements
setupBroadcastReceiver(intentFilter);
this.recordManager = new HistoryRecordManager(context);
this.sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
this.progressUpdateReactor = new SerialDisposable();
this.databaseUpdateReactor = new CompositeDisposable();
@ -223,7 +233,7 @@ public abstract class BasePlayer implements
public void setup() {
if (simpleExoPlayer == null) {
initPlayer(/*playOnInit=*/true);
initPlayer(true);
}
initListeners();
}
@ -250,7 +260,8 @@ public abstract class BasePlayer implements
registerBroadcastReceiver();
}
public void initListeners() { }
public void initListeners() {
}
public void handleIntent(final Intent intent) {
if (DEBUG) {
@ -288,34 +299,72 @@ public abstract class BasePlayer implements
final float playbackPitch = savedParameters.pitch;
final boolean playbackSkipSilence = savedParameters.skipSilence;
final boolean samePlayQueue = playQueue != null && playQueue.equals(queue);
final int repeatMode = intent.getIntExtra(REPEAT_MODE, getRepeatMode());
final boolean isMuted = intent
.getBooleanExtra(IS_MUTED, simpleExoPlayer != null && isMuted());
/*
* There are 3 situations when playback shouldn't be started from scratch (zero timestamp):
* 1. User pressed on a timestamp link and the same video should be rewound to the timestamp
* 2. User changed a player from, for example. main to popup, or from audio to main, etc
* 3. User chose to resume a video based on a saved timestamp from history of played videos
* In those cases time will be saved because re-init of the play queue is a not an instant
* task and requires network calls
* */
// seek to timestamp if stream is already playing
if (simpleExoPlayer != null
&& queue.size() == 1
&& playQueue != null
&& playQueue.size() == 1
&& playQueue.getItem() != null
&& queue.getItem().getUrl().equals(playQueue.getItem().getUrl())
&& queue.getItem().getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET
) {
&& queue.getItem().getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET) {
// Player can have state = IDLE when playback is stopped or failed
// and we should retry() in this case
if (simpleExoPlayer.getPlaybackState() == Player.STATE_IDLE) {
simpleExoPlayer.retry();
}
simpleExoPlayer.seekTo(playQueue.getIndex(), queue.getItem().getRecoveryPosition());
return;
} else if (intent.getBooleanExtra(RESUME_PLAYBACK, false) && isPlaybackResumeEnabled()) {
} else if (samePlayQueue && !playQueue.isDisposed() && simpleExoPlayer != null) {
// Do not re-init the same PlayQueue. Save time
// Player can have state = IDLE when playback is stopped or failed
// and we should retry() in this case
if (simpleExoPlayer.getPlaybackState() == Player.STATE_IDLE) {
simpleExoPlayer.retry();
}
return;
} else if (intent.getBooleanExtra(RESUME_PLAYBACK, false)
&& isPlaybackResumeEnabled()
&& !samePlayQueue) {
final PlayQueueItem item = queue.getItem();
if (item != null && item.getRecoveryPosition() == PlayQueueItem.RECOVERY_UNSET) {
stateLoader = recordManager.loadStreamState(item)
.observeOn(mainThread())
.doFinally(() -> initPlayback(queue, repeatMode, playbackSpeed,
playbackPitch, playbackSkipSilence, true, isMuted))
.observeOn(AndroidSchedulers.mainThread())
// Do not place initPlayback() in doFinally() because
// it restarts playback after destroy()
//.doFinally()
.subscribe(
state -> queue
.setRecovery(queue.getIndex(), state.getProgressTime()),
state -> {
queue.setRecovery(queue.getIndex(), state.getProgressTime());
initPlayback(queue, repeatMode, playbackSpeed, playbackPitch,
playbackSkipSilence, true, isMuted);
},
error -> {
if (DEBUG) {
error.printStackTrace();
}
// In case any error we can start playback without history
initPlayback(queue, repeatMode, playbackSpeed, playbackPitch,
playbackSkipSilence, true, isMuted);
},
() -> {
// Completed but not found in history
initPlayback(queue, repeatMode, playbackSpeed, playbackPitch,
playbackSkipSilence, true, isMuted);
}
);
databaseUpdateReactor.add(stateLoader);
@ -323,8 +372,11 @@ public abstract class BasePlayer implements
}
}
// Good to go...
initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, playbackSkipSilence,
/*playOnInit=*/!intent.getBooleanExtra(START_PAUSED, false), isMuted);
// In a case of equal PlayQueues we can re-init old one but only when it is disposed
initPlayback(samePlayQueue ? playQueue : queue, repeatMode,
playbackSpeed, playbackPitch, playbackSkipSilence,
!intent.getBooleanExtra(START_PAUSED, false),
isMuted);
}
private PlaybackParameters retrievePlaybackParametersFromPreferences() {
@ -410,6 +462,7 @@ public abstract class BasePlayer implements
databaseUpdateReactor.clear();
progressUpdateReactor.set(null);
ImageLoader.getInstance().stop();
}
/*//////////////////////////////////////////////////////////////////////////
@ -561,7 +614,8 @@ public abstract class BasePlayer implements
}
}
public void onPausedSeek() { }
public void onPausedSeek() {
}
public void onCompleted() {
if (DEBUG) {
@ -1089,6 +1143,7 @@ public abstract class BasePlayer implements
}
simpleExoPlayer.setPlayWhenReady(true);
savePlaybackState();
}
public void onPause() {
@ -1101,6 +1156,7 @@ public abstract class BasePlayer implements
audioReactor.abandonAudioFocus();
simpleExoPlayer.setPlayWhenReady(false);
savePlaybackState();
}
public void onPlayPause() {
@ -1433,6 +1489,10 @@ public abstract class BasePlayer implements
return simpleExoPlayer != null && simpleExoPlayer.isPlaying();
}
public boolean isLoading() {
return simpleExoPlayer != null && simpleExoPlayer.isLoading();
}
@Player.RepeatMode
public int getRepeatMode() {
return simpleExoPlayer == null
@ -1473,8 +1533,9 @@ public abstract class BasePlayer implements
/**
* Sets the playback parameters of the player, and also saves them to shared preferences.
* Speed and pitch are rounded up to 2 decimal places before being used or saved.
* @param speed the playback speed, will be rounded to up to 2 decimal places
* @param pitch the playback pitch, will be rounded to up to 2 decimal places
*
* @param speed the playback speed, will be rounded to up to 2 decimal places
* @param pitch the playback pitch, will be rounded to up to 2 decimal places
* @param skipSilence skip silence during playback
*/
public void setPlaybackParameters(final float speed, final float pitch,
@ -1490,11 +1551,11 @@ public abstract class BasePlayer implements
private void savePlaybackParametersToPreferences(final float speed, final float pitch,
final boolean skipSilence) {
PreferenceManager.getDefaultSharedPreferences(context)
.edit()
.putFloat(context.getString(R.string.playback_speed_key), speed)
.putFloat(context.getString(R.string.playback_pitch_key), pitch)
.putBoolean(context.getString(R.string.playback_skip_silence_key), skipSilence)
.apply();
.edit()
.putFloat(context.getString(R.string.playback_speed_key), speed)
.putFloat(context.getString(R.string.playback_pitch_key), pitch)
.putBoolean(context.getString(R.string.playback_skip_silence_key), skipSilence)
.apply();
}
public PlayQueue getPlayQueue() {

View file

@ -0,0 +1,263 @@
/*
* Copyright 2017 Mauricio Colli <mauriciocolli@outlook.com>
* Part of NewPipe
*
* License: GPL-3.0+
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.schabi.newpipe.player;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import org.schabi.newpipe.R;
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";
private static final boolean DEBUG = BasePlayer.DEBUG;
private VideoPlayerImpl playerImpl;
private WindowManager windowManager;
private final IBinder mBinder = new MainPlayer.LocalBinder();
public enum PlayerType {
VIDEO,
AUDIO,
POPUP
}
/*//////////////////////////////////////////////////////////////////////////
// Notification
//////////////////////////////////////////////////////////////////////////*/
static final String ACTION_CLOSE
= "org.schabi.newpipe.player.MainPlayer.CLOSE";
static final String ACTION_PLAY_PAUSE
= "org.schabi.newpipe.player.MainPlayer.PLAY_PAUSE";
static final String ACTION_OPEN_CONTROLS
= "org.schabi.newpipe.player.MainPlayer.OPEN_CONTROLS";
static final String ACTION_REPEAT
= "org.schabi.newpipe.player.MainPlayer.REPEAT";
static final String ACTION_PLAY_NEXT
= "org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_NEXT";
static final String ACTION_PLAY_PREVIOUS
= "org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PREVIOUS";
static final String ACTION_FAST_REWIND
= "org.schabi.newpipe.player.MainPlayer.ACTION_FAST_REWIND";
static final String ACTION_FAST_FORWARD
= "org.schabi.newpipe.player.MainPlayer.ACTION_FAST_FORWARD";
static final String ACTION_BUFFERING
= "org.schabi.newpipe.player.BackgroundPlayer.ACTION_BUFFERING";
static final String ACTION_SHUFFLE
= "org.schabi.newpipe.player.BackgroundPlayer.ACTION_SHUFFLE";
static final String SET_IMAGE_RESOURCE_METHOD = "setImageResource";
/*//////////////////////////////////////////////////////////////////////////
// Service's LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreate() {
if (DEBUG) {
Log.d(TAG, "onCreate() called");
}
assureCorrectAppLanguage(this);
windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
ThemeHelper.setTheme(this);
createView();
}
private void createView() {
final View layout = View.inflate(this, R.layout.player, null);
playerImpl = new VideoPlayerImpl(this);
playerImpl.setup(layout);
playerImpl.shouldUpdateOnProgress = true;
}
@Override
public int onStartCommand(final Intent intent, final int flags, final int startId) {
if (DEBUG) {
Log.d(TAG, "onStartCommand() called with: intent = [" + intent
+ "], flags = [" + flags + "], startId = [" + startId + "]");
}
if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
&& playerImpl.playQueue == null) {
// Player is not working, no need to process media button's action
return START_NOT_STICKY;
}
if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
|| intent.getStringExtra(VideoPlayer.PLAY_QUEUE_KEY) != null) {
showNotificationAndStartForeground();
}
playerImpl.handleIntent(intent);
if (playerImpl.mediaSessionManager != null) {
playerImpl.mediaSessionManager.handleMediaButtonIntent(intent);
}
return START_NOT_STICKY;
}
public void stop(final boolean autoplayEnabled) {
if (DEBUG) {
Log.d(TAG, "stop() called");
}
if (playerImpl.getPlayer() != null) {
playerImpl.wasPlaying = playerImpl.getPlayer().getPlayWhenReady();
// Releases wifi & cpu, disables keepScreenOn, etc.
if (!autoplayEnabled) {
playerImpl.onPause();
}
// We can't just pause the player here because it will make transition
// from one stream to a new stream not smooth
playerImpl.getPlayer().stop(false);
playerImpl.setRecovery();
// Android TV will handle back button in case controls will be visible
// (one more additional unneeded click while the player is hidden)
playerImpl.hideControls(0, 0);
// 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
if (!autoplayEnabled) {
stopForeground(true);
}
}
}
@Override
public void onTaskRemoved(final Intent rootIntent) {
super.onTaskRemoved(rootIntent);
onDestroy();
// Unload from memory completely
Runtime.getRuntime().halt(0);
}
@Override
public void onDestroy() {
if (DEBUG) {
Log.d(TAG, "destroy() called");
}
onClose();
}
@Override
protected void attachBaseContext(final Context base) {
super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base));
}
@Override
public IBinder onBind(final Intent intent) {
return mBinder;
}
/*//////////////////////////////////////////////////////////////////////////
// Actions
//////////////////////////////////////////////////////////////////////////*/
private void onClose() {
if (DEBUG) {
Log.d(TAG, "onClose() called");
}
if (playerImpl != null) {
removeViewFromParent();
playerImpl.setRecovery();
playerImpl.savePlaybackState();
playerImpl.stopActivityBinding();
playerImpl.removePopupFromView();
playerImpl.destroy();
}
NotificationUtil.getInstance().cancelNotification();
stopForeground(true);
stopSelf();
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
boolean isLandscape() {
// DisplayMetrics from activity context knows about MultiWindow feature
// while DisplayMetrics from app context doesn't
final DisplayMetrics metrics = (playerImpl != null
&& playerImpl.getParentActivity() != null)
? playerImpl.getParentActivity().getResources().getDisplayMetrics()
: getResources().getDisplayMetrics();
return metrics.heightPixels < metrics.widthPixels;
}
public View getView() {
if (playerImpl == null) {
return null;
}
return playerImpl.getRootView();
}
public void removeViewFromParent() {
if (getView().getParent() != null) {
if (playerImpl.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());
}
}
}
private void showNotificationAndStartForeground() {
NotificationUtil.getInstance().recreateBackgroundPlayerNotification(playerImpl, true);
NotificationUtil.getInstance().setProgressbarOnOldNotifications(100, 0, false);
NotificationUtil.getInstance().startForegroundServiceWithNotification(this);
}
public class LocalBinder extends Binder {
public MainPlayer getService() {
return MainPlayer.this;
}
public VideoPlayerImpl getPlayer() {
return MainPlayer.this.playerImpl;
}
}
}

View file

@ -1,66 +0,0 @@
package org.schabi.newpipe.player;
import android.content.Intent;
import android.view.MenuItem;
import org.schabi.newpipe.R;
import static org.schabi.newpipe.player.PopupVideoPlayer.ACTION_CLOSE;
public final class PopupVideoPlayerActivity extends ServicePlayerActivity {
private static final String TAG = "PopupVideoPlayerActivity";
@Override
public String getTag() {
return TAG;
}
@Override
public String getSupportActionTitle() {
return getResources().getString(R.string.title_activity_popup_player);
}
@Override
public Intent getBindIntent() {
return new Intent(this, PopupVideoPlayer.class);
}
@Override
public void startPlayerListener() {
if (player != null && player instanceof PopupVideoPlayer.VideoPlayerImpl) {
((PopupVideoPlayer.VideoPlayerImpl) player).setActivityListener(this);
}
}
@Override
public void stopPlayerListener() {
if (player != null && player instanceof PopupVideoPlayer.VideoPlayerImpl) {
((PopupVideoPlayer.VideoPlayerImpl) player).removeActivityListener(this);
}
}
@Override
public int getPlayerOptionMenuResource() {
return R.menu.menu_play_queue_popup;
}
@Override
public boolean onPlayerOptionSelected(final MenuItem item) {
if (item.getItemId() == R.id.action_switch_background) {
this.player.setRecovery();
getApplicationContext().sendBroadcast(getPlayerShutdownIntent());
getApplicationContext().startService(
getSwitchIntent(BackgroundPlayer.class)
.putExtra(BasePlayer.START_PAUSED, !this.player.isPlaying())
);
return true;
}
return false;
}
@Override
public Intent getPlayerShutdownIntent() {
return new Intent(ACTION_CLOSE);
}
}

View file

@ -27,17 +27,21 @@ import androidx.recyclerview.widget.RecyclerView;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
import org.schabi.newpipe.player.event.PlayerEventListener;
import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
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.Constants;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ThemeHelper;
@ -110,7 +114,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
public abstract boolean onPlayerOptionSelected(MenuItem item);
public abstract Intent getPlayerShutdownIntent();
public abstract void setupMenu(Menu m);
////////////////////////////////////////////////////////////////////////////
// Activity Lifecycle
////////////////////////////////////////////////////////////////////////////
@ -152,6 +156,13 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
return true;
}
// Allow to setup visibility of menuItems
@Override
public boolean onPrepareOptionsMenu(final Menu m) {
setupMenu(m);
return super.onPrepareOptionsMenu(m);
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
@ -175,11 +186,9 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
return true;
case R.id.action_switch_main:
this.player.setRecovery();
getApplicationContext().sendBroadcast(getPlayerShutdownIntent());
getApplicationContext().startActivity(
getSwitchIntent(MainVideoPlayer.class)
.putExtra(BasePlayer.START_PAUSED, !this.player.isPlaying())
);
getSwitchIntent(MainActivity.class, MainPlayer.PlayerType.VIDEO)
.putExtra(BasePlayer.START_PAUSED, !this.player.isPlaying()));
return true;
}
return onPlayerOptionSelected(item) || super.onOptionsItemSelected(item);
@ -191,13 +200,22 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
unbind();
}
protected Intent getSwitchIntent(final Class clazz) {
protected Intent getSwitchIntent(final Class clazz, final MainPlayer.PlayerType playerType) {
return NavigationHelper.getPlayerIntent(getApplicationContext(), clazz,
this.player.getPlayQueue(), this.player.getRepeatMode(),
this.player.getPlaybackSpeed(), this.player.getPlaybackPitch(),
this.player.getPlaybackSkipSilence(), null, false, false, this.player.isMuted())
this.player.getPlaybackSkipSilence(),
null,
true,
!this.player.isPlaying(),
this.player.isMuted())
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(BasePlayer.START_PAUSED, !this.player.isPlaying());
.putExtra(Constants.KEY_LINK_TYPE, StreamingService.LinkType.STREAM)
.putExtra(Constants.KEY_URL, this.player.getVideoUrl())
.putExtra(Constants.KEY_TITLE, this.player.getVideoTitle())
.putExtra(Constants.KEY_SERVICE_ID,
this.player.getCurrentMetadata().getMetadata().getServiceId())
.putExtra(VideoPlayer.PLAYER_TYPE, playerType);
}
////////////////////////////////////////////////////////////////////////////
@ -247,6 +265,8 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
if (service instanceof PlayerServiceBinder) {
player = ((PlayerServiceBinder) service).getPlayerInstance();
} else if (service instanceof MainPlayer.LocalBinder) {
player = ((MainPlayer.LocalBinder) service).getPlayer();
}
if (player == null || player.getPlayQueue() == null
@ -500,7 +520,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
return;
}
PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), player.getPlaybackPitch(),
player.getPlaybackSkipSilence()).show(getSupportFragmentManager(), getTag());
player.getPlaybackSkipSilence(), this).show(getSupportFragmentManager(), getTag());
}
@Override
@ -571,6 +591,10 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
// Binding Service Listener
////////////////////////////////////////////////////////////////////////////
@Override
public void onQueueUpdate(final PlayQueue queue) {
}
@Override
public void onPlaybackUpdate(final int state, final int repeatMode, final boolean shuffled,
final PlaybackParameters parameters) {
@ -610,7 +634,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
}
@Override
public void onMetadataUpdate(final StreamInfo info) {
public void onMetadataUpdate(final StreamInfo info, final PlayQueue queue) {
if (info != null) {
metadataTitle.setText(info.getName());
metadataArtist.setText(info.getUploaderName());

View file

@ -34,16 +34,16 @@ import android.os.Build;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.SurfaceView;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.PopupMenu;
import android.widget.ProgressBar;
import android.widget.SeekBar;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.content.res.AppCompatResources;
@ -69,6 +69,7 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.player.resolver.MediaSourceTag;
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver;
import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.views.ExpandableSurfaceView;
import java.util.ArrayList;
import java.util.List;
@ -117,8 +118,7 @@ public abstract class VideoPlayer extends BasePlayer
private View rootView;
private AspectRatioFrameLayout aspectRatioFrameLayout;
private SurfaceView surfaceView;
private ExpandableSurfaceView surfaceView;
private View surfaceForeground;
private View loadingPanel;
@ -135,7 +135,7 @@ public abstract class VideoPlayer extends BasePlayer
private TextView playbackLiveSync;
private TextView playbackSpeedTextView;
private View topControlsRoot;
private LinearLayout topControlsRoot;
private TextView qualityTextView;
private SubtitleView subtitleView;
@ -182,7 +182,6 @@ public abstract class VideoPlayer extends BasePlayer
public void initViews(final View view) {
this.rootView = view;
this.aspectRatioFrameLayout = view.findViewById(R.id.aspectRatioLayout);
this.surfaceView = view.findViewById(R.id.surfaceView);
this.surfaceForeground = view.findViewById(R.id.surfaceForeground);
this.loadingPanel = view.findViewById(R.id.loading_panel);
@ -207,12 +206,10 @@ public abstract class VideoPlayer extends BasePlayer
this.resizeView = view.findViewById(R.id.resizeTextView);
resizeView.setText(PlayerHelper
.resizeTypeOf(context, aspectRatioFrameLayout.getResizeMode()));
.resizeTypeOf(context, getSurfaceView().getResizeMode()));
this.captionTextView = view.findViewById(R.id.captionTextView);
//this.aspectRatioFrameLayout.setAspectRatio(16.0f / 9.0f);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN);
}
@ -520,7 +517,6 @@ public abstract class VideoPlayer extends BasePlayer
super.onCompleted();
showControls(500);
animateView(endScreen, true, 800);
animateView(currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, false, 200);
loadingPanel.setVisibility(View.GONE);
@ -555,7 +551,7 @@ public abstract class VideoPlayer extends BasePlayer
+ "unappliedRotationDegrees = [" + unappliedRotationDegrees + "], "
+ "pixelWidthHeightRatio = [" + pixelWidthHeightRatio + "]");
}
aspectRatioFrameLayout.setAspectRatio(((float) width) / height);
getSurfaceView().setAspectRatio(((float) width) / height);
}
@Override
@ -620,12 +616,6 @@ public abstract class VideoPlayer extends BasePlayer
playbackSpeedTextView.setText(formatSpeed(getPlaybackSpeed()));
super.onPrepared(playWhenReady);
if (simpleExoPlayer.getCurrentPosition() != 0 && !isControlsVisible()) {
controlsVisibilityHandler.removeCallbacksAndMessages(null);
controlsVisibilityHandler
.postDelayed(this::showControlsThenHide, DEFAULT_CONTROLS_DURATION);
}
}
@Override
@ -675,7 +665,7 @@ public abstract class VideoPlayer extends BasePlayer
}
}
protected void onFullScreenButtonClicked() {
protected void toggleFullscreen() {
changeState(STATE_BLOCKED);
}
@ -799,16 +789,16 @@ public abstract class VideoPlayer extends BasePlayer
showControls(DEFAULT_CONTROLS_DURATION);
}
private void onResizeClicked() {
if (getAspectRatioFrameLayout() != null) {
final int currentResizeMode = getAspectRatioFrameLayout().getResizeMode();
void onResizeClicked() {
if (getSurfaceView() != null) {
final int currentResizeMode = getSurfaceView().getResizeMode();
final int newResizeMode = nextResizeMode(currentResizeMode);
setResizeMode(newResizeMode);
}
}
protected void setResizeMode(@AspectRatioFrameLayout.ResizeMode final int resizeMode) {
getAspectRatioFrameLayout().setResizeMode(resizeMode);
getSurfaceView().setResizeMode(resizeMode);
getResizeView().setText(PlayerHelper.resizeTypeOf(context, resizeMode));
}
@ -916,9 +906,9 @@ public abstract class VideoPlayer extends BasePlayer
if (drawableId == -1) {
if (controlAnimationView.getVisibility() == View.VISIBLE) {
controlViewAnimator = ObjectAnimator.ofPropertyValuesHolder(controlAnimationView,
PropertyValuesHolder.ofFloat(View.ALPHA, 1f, 0f),
PropertyValuesHolder.ofFloat(View.SCALE_X, 1.4f, 1f),
PropertyValuesHolder.ofFloat(View.SCALE_Y, 1.4f, 1f)
PropertyValuesHolder.ofFloat(View.ALPHA, 1.0f, 0.0f),
PropertyValuesHolder.ofFloat(View.SCALE_X, 1.4f, 1.0f),
PropertyValuesHolder.ofFloat(View.SCALE_Y, 1.4f, 1.0f)
).setDuration(DEFAULT_CONTROLS_DURATION);
controlViewAnimator.addListener(new AnimatorListenerAdapter() {
@Override
@ -1020,6 +1010,9 @@ public abstract class VideoPlayer extends BasePlayer
animateView(controlsRoot, false, duration);
};
}
public abstract void hideSystemUIIfNeeded();
/*//////////////////////////////////////////////////////////////////////////
// Getters and Setters
//////////////////////////////////////////////////////////////////////////*/
@ -1033,11 +1026,7 @@ public abstract class VideoPlayer extends BasePlayer
this.resolver.setPlaybackQuality(quality);
}
public AspectRatioFrameLayout getAspectRatioFrameLayout() {
return aspectRatioFrameLayout;
}
public SurfaceView getSurfaceView() {
public ExpandableSurfaceView getSurfaceView() {
return surfaceView;
}
@ -1096,7 +1085,7 @@ public abstract class VideoPlayer extends BasePlayer
return playbackEndTime;
}
public View getTopControlsRoot() {
public LinearLayout getTopControlsRoot() {
return topControlsRoot;
}
@ -1108,6 +1097,10 @@ public abstract class VideoPlayer extends BasePlayer
return qualityPopupMenu;
}
public TextView getPlaybackSpeedTextView() {
return playbackSpeedTextView;
}
public PopupMenu getPlaybackSpeedPopupMenu() {
return playbackSpeedPopupMenu;
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,64 @@
package org.schabi.newpipe.player.event;
import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import org.schabi.newpipe.R;
import java.util.Arrays;
import java.util.List;
public class CustomBottomSheetBehavior extends BottomSheetBehavior<FrameLayout> {
public CustomBottomSheetBehavior(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
boolean visible;
Rect globalRect = new Rect();
private boolean skippingInterception = false;
private final List<Integer> skipInterceptionOfElements = Arrays.asList(
R.id.detail_content_root_layout, R.id.relatedStreamsLayout,
R.id.playQueuePanel, R.id.viewpager);
@Override
public boolean onInterceptTouchEvent(@NonNull final CoordinatorLayout parent,
@NonNull final FrameLayout child,
final MotionEvent event) {
// Drop following when action ends
if (event.getAction() == MotionEvent.ACTION_CANCEL
|| event.getAction() == MotionEvent.ACTION_UP) {
skippingInterception = false;
}
// Found that user still swiping, continue following
if (skippingInterception) {
return false;
}
// Don't need to do anything if bottomSheet isn't expanded
if (getState() == BottomSheetBehavior.STATE_EXPANDED) {
// Without overriding scrolling will not work when user touches these elements
for (final Integer element : skipInterceptionOfElements) {
final ViewGroup viewGroup = child.findViewById(element);
if (viewGroup != null) {
visible = viewGroup.getGlobalVisibleRect(globalRect);
if (visible
&& globalRect.contains((int) event.getRawX(), (int) event.getRawY())) {
skippingInterception = true;
return false;
}
}
}
}
return super.onInterceptTouchEvent(parent, child, event);
}
}

View file

@ -0,0 +1,5 @@
package org.schabi.newpipe.player.event;
public interface OnKeyDownListener {
boolean onKeyDown(int keyCode);
}

View file

@ -4,14 +4,13 @@ package org.schabi.newpipe.player.event;
import com.google.android.exoplayer2.PlaybackParameters;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.player.playqueue.PlayQueue;
public interface PlayerEventListener {
void onQueueUpdate(PlayQueue queue);
void onPlaybackUpdate(int state, int repeatMode, boolean shuffled,
PlaybackParameters parameters);
void onProgressUpdate(int currentProgress, int duration, int bufferPercent);
void onMetadataUpdate(StreamInfo info);
void onMetadataUpdate(StreamInfo info, PlayQueue queue);
void onServiceStopped();
}

View file

@ -0,0 +1,622 @@
package org.schabi.newpipe.player.event;
import android.app.Activity;
import android.content.Context;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.Window;
import android.view.WindowManager;
import androidx.appcompat.content.res.AppCompatResources;
import org.schabi.newpipe.R;
import org.schabi.newpipe.player.BasePlayer;
import org.schabi.newpipe.player.MainPlayer;
import org.schabi.newpipe.player.VideoPlayerImpl;
import org.schabi.newpipe.player.helper.PlayerHelper;
import static org.schabi.newpipe.player.BasePlayer.STATE_PLAYING;
import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_DURATION;
import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME;
import static org.schabi.newpipe.util.AnimationUtils.Type.SCALE_AND_ALPHA;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
public class PlayerGestureListener
extends GestureDetector.SimpleOnGestureListener
implements View.OnTouchListener {
private static final String TAG = ".PlayerGestureListener";
private static final boolean DEBUG = BasePlayer.DEBUG;
private final VideoPlayerImpl playerImpl;
private final MainPlayer service;
private int initialPopupX;
private int initialPopupY;
private boolean isMovingInMain;
private boolean isMovingInPopup;
private boolean isResizing;
private final int tossFlingVelocity;
private final boolean isVolumeGestureEnabled;
private final boolean isBrightnessGestureEnabled;
private final int maxVolume;
private static final int MOVEMENT_THRESHOLD = 40;
// [popup] initial coordinates and distance between fingers
private double initPointerDistance = -1;
private float initFirstPointerX = -1;
private float initFirstPointerY = -1;
private float initSecPointerX = -1;
private float initSecPointerY = -1;
public PlayerGestureListener(final VideoPlayerImpl playerImpl, final MainPlayer service) {
this.playerImpl = playerImpl;
this.service = service;
this.tossFlingVelocity = PlayerHelper.getTossFlingVelocity(service);
isVolumeGestureEnabled = PlayerHelper.isVolumeGestureEnabled(service);
isBrightnessGestureEnabled = PlayerHelper.isBrightnessGestureEnabled(service);
maxVolume = playerImpl.getAudioReactor().getMaxVolume();
}
/*//////////////////////////////////////////////////////////////////////////
// Helpers
//////////////////////////////////////////////////////////////////////////*/
/*
* Main and popup players' gesture listeners is too different.
* So it will be better to have different implementations of them
* */
@Override
public boolean onDoubleTap(final MotionEvent e) {
if (DEBUG) {
Log.d(TAG, "onDoubleTap() called with: e = [" + e + "]" + "rawXy = "
+ e.getRawX() + ", " + e.getRawY() + ", xy = " + e.getX() + ", " + e.getY());
}
if (playerImpl.popupPlayerSelected()) {
return onDoubleTapInPopup(e);
} else {
return onDoubleTapInMain(e);
}
}
@Override
public boolean onSingleTapConfirmed(final MotionEvent e) {
if (DEBUG) {
Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]");
}
if (playerImpl.popupPlayerSelected()) {
return onSingleTapConfirmedInPopup(e);
} else {
return onSingleTapConfirmedInMain(e);
}
}
@Override
public boolean onDown(final MotionEvent e) {
if (DEBUG) {
Log.d(TAG, "onDown() called with: e = [" + e + "]");
}
if (playerImpl.popupPlayerSelected()) {
return onDownInPopup(e);
} else {
return true;
}
}
@Override
public void onLongPress(final MotionEvent e) {
if (DEBUG) {
Log.d(TAG, "onLongPress() called with: e = [" + e + "]");
}
if (playerImpl.popupPlayerSelected()) {
onLongPressInPopup(e);
}
}
@Override
public boolean onScroll(final MotionEvent initialEvent, final MotionEvent movingEvent,
final float distanceX, final float distanceY) {
if (playerImpl.popupPlayerSelected()) {
return onScrollInPopup(initialEvent, movingEvent, distanceX, distanceY);
} else {
return onScrollInMain(initialEvent, movingEvent, distanceX, distanceY);
}
}
@Override
public boolean onFling(final MotionEvent e1, final MotionEvent e2,
final float velocityX, final float velocityY) {
if (DEBUG) {
Log.d(TAG, "onFling() called with velocity: dX=["
+ velocityX + "], dY=[" + velocityY + "]");
}
if (playerImpl.popupPlayerSelected()) {
return onFlingInPopup(e1, e2, velocityX, velocityY);
} else {
return true;
}
}
@Override
public boolean onTouch(final View v, final MotionEvent event) {
/*if (DEBUG && false) {
Log.d(TAG, "onTouch() called with: v = [" + v + "], event = [" + event + "]");
}*/
if (playerImpl.popupPlayerSelected()) {
return onTouchInPopup(v, event);
} else {
return onTouchInMain(v, event);
}
}
/*//////////////////////////////////////////////////////////////////////////
// Main player listener
//////////////////////////////////////////////////////////////////////////*/
private boolean onDoubleTapInMain(final MotionEvent e) {
if (e.getX() > playerImpl.getRootView().getWidth() * 2.0 / 3.0) {
playerImpl.onFastForward();
} else if (e.getX() < playerImpl.getRootView().getWidth() / 3.0) {
playerImpl.onFastRewind();
} else {
playerImpl.getPlayPauseButton().performClick();
}
return true;
}
private boolean onSingleTapConfirmedInMain(final MotionEvent e) {
if (DEBUG) {
Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]");
}
if (playerImpl.getCurrentState() == BasePlayer.STATE_BLOCKED) {
return true;
}
if (playerImpl.isControlsVisible()) {
playerImpl.hideControls(150, 0);
} else {
if (playerImpl.getCurrentState() == BasePlayer.STATE_COMPLETED) {
playerImpl.showControls(0);
} else {
playerImpl.showControlsThenHide();
}
}
return true;
}
private boolean onScrollInMain(final MotionEvent initialEvent, final MotionEvent movingEvent,
final float distanceX, final float distanceY) {
if (!isVolumeGestureEnabled && !isBrightnessGestureEnabled) {
return false;
}
final boolean isTouchingStatusBar = initialEvent.getY() < getStatusBarHeight(service);
final boolean isTouchingNavigationBar = initialEvent.getY()
> playerImpl.getRootView().getHeight() - getNavigationBarHeight(service);
if (isTouchingStatusBar || isTouchingNavigationBar) {
return false;
}
/*if (DEBUG && false) Log.d(TAG, "onScrollInMain = " +
", e1.getRaw = [" + initialEvent.getRawX() + ", " + initialEvent.getRawY() + "]" +
", e2.getRaw = [" + movingEvent.getRawX() + ", " + movingEvent.getRawY() + "]" +
", distanceXy = [" + distanceX + ", " + distanceY + "]");*/
final boolean insideThreshold =
Math.abs(movingEvent.getY() - initialEvent.getY()) <= MOVEMENT_THRESHOLD;
if (!isMovingInMain && (insideThreshold || Math.abs(distanceX) > Math.abs(distanceY))
|| playerImpl.getCurrentState() == BasePlayer.STATE_COMPLETED) {
return false;
}
isMovingInMain = true;
boolean acceptAnyArea = isVolumeGestureEnabled != isBrightnessGestureEnabled;
boolean acceptVolumeArea = acceptAnyArea
|| initialEvent.getX() > playerImpl.getRootView().getWidth() / 2.0;
if (isVolumeGestureEnabled && acceptVolumeArea) {
playerImpl.getVolumeProgressBar().incrementProgressBy((int) distanceY);
final float currentProgressPercent = (float) playerImpl
.getVolumeProgressBar().getProgress() / playerImpl.getMaxGestureLength();
final int currentVolume = (int) (maxVolume * currentProgressPercent);
playerImpl.getAudioReactor().setVolume(currentVolume);
if (DEBUG) {
Log.d(TAG, "onScroll().volumeControl, currentVolume = " + currentVolume);
}
playerImpl.getVolumeImageView().setImageDrawable(
AppCompatResources.getDrawable(service, currentProgressPercent <= 0
? R.drawable.ic_volume_off_white_24dp
: currentProgressPercent < 0.25 ? R.drawable.ic_volume_mute_white_24dp
: currentProgressPercent < 0.75 ? R.drawable.ic_volume_down_white_24dp
: R.drawable.ic_volume_up_white_24dp)
);
if (playerImpl.getVolumeRelativeLayout().getVisibility() != View.VISIBLE) {
animateView(playerImpl.getVolumeRelativeLayout(), SCALE_AND_ALPHA, true, 200);
}
if (playerImpl.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) {
playerImpl.getBrightnessRelativeLayout().setVisibility(View.GONE);
}
} else {
final Activity parent = playerImpl.getParentActivity();
if (parent == null) {
return true;
}
final Window window = parent.getWindow();
playerImpl.getBrightnessProgressBar().incrementProgressBy((int) distanceY);
final float currentProgressPercent = (float) playerImpl.getBrightnessProgressBar()
.getProgress() / playerImpl.getMaxGestureLength();
final WindowManager.LayoutParams layoutParams = window.getAttributes();
layoutParams.screenBrightness = currentProgressPercent;
window.setAttributes(layoutParams);
if (DEBUG) {
Log.d(TAG, "onScroll().brightnessControl, "
+ "currentBrightness = " + currentProgressPercent);
}
playerImpl.getBrightnessImageView().setImageDrawable(
AppCompatResources.getDrawable(service,
currentProgressPercent < 0.25
? R.drawable.ic_brightness_low_white_24dp
: currentProgressPercent < 0.75
? R.drawable.ic_brightness_medium_white_24dp
: R.drawable.ic_brightness_high_white_24dp)
);
if (playerImpl.getBrightnessRelativeLayout().getVisibility() != View.VISIBLE) {
animateView(playerImpl.getBrightnessRelativeLayout(), SCALE_AND_ALPHA, true, 200);
}
if (playerImpl.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) {
playerImpl.getVolumeRelativeLayout().setVisibility(View.GONE);
}
}
return true;
}
private void onScrollEndInMain() {
if (DEBUG) {
Log.d(TAG, "onScrollEnd() called");
}
if (playerImpl.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) {
animateView(playerImpl.getVolumeRelativeLayout(), SCALE_AND_ALPHA, false, 200, 200);
}
if (playerImpl.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) {
animateView(playerImpl.getBrightnessRelativeLayout(), SCALE_AND_ALPHA, false, 200, 200);
}
if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) {
playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
}
}
private boolean onTouchInMain(final View v, final MotionEvent event) {
playerImpl.getGestureDetector().onTouchEvent(event);
if (event.getAction() == MotionEvent.ACTION_UP && isMovingInMain) {
isMovingInMain = false;
onScrollEndInMain();
}
// This hack allows to stop receiving touch events on appbar
// while touching video player's view
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
v.getParent().requestDisallowInterceptTouchEvent(playerImpl.isFullscreen());
return true;
case MotionEvent.ACTION_UP:
v.getParent().requestDisallowInterceptTouchEvent(false);
return false;
default:
return true;
}
}
/*//////////////////////////////////////////////////////////////////////////
// Popup player listener
//////////////////////////////////////////////////////////////////////////*/
private boolean onDoubleTapInPopup(final MotionEvent e) {
if (playerImpl == null || !playerImpl.isPlaying()) {
return false;
}
playerImpl.hideControls(0, 0);
if (e.getX() > playerImpl.getPopupWidth() / 2) {
playerImpl.onFastForward();
} else {
playerImpl.onFastRewind();
}
return true;
}
private boolean onSingleTapConfirmedInPopup(final MotionEvent e) {
if (playerImpl == null || playerImpl.getPlayer() == null) {
return false;
}
if (playerImpl.isControlsVisible()) {
playerImpl.hideControls(100, 100);
} else {
playerImpl.getPlayPauseButton().requestFocus();
playerImpl.showControlsThenHide();
}
return true;
}
private boolean onDownInPopup(final MotionEvent e) {
// 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).
playerImpl.updateScreenSize();
playerImpl.checkPopupPositionBounds();
initialPopupX = playerImpl.getPopupLayoutParams().x;
initialPopupY = playerImpl.getPopupLayoutParams().y;
playerImpl.setPopupWidth(playerImpl.getPopupLayoutParams().width);
playerImpl.setPopupHeight(playerImpl.getPopupLayoutParams().height);
return super.onDown(e);
}
private void onLongPressInPopup(final MotionEvent e) {
playerImpl.updateScreenSize();
playerImpl.checkPopupPositionBounds();
playerImpl.updatePopupSize((int) playerImpl.getScreenWidth(), -1);
}
private boolean onScrollInPopup(final MotionEvent initialEvent,
final MotionEvent movingEvent,
final float distanceX,
final float distanceY) {
if (isResizing || playerImpl == null) {
return super.onScroll(initialEvent, movingEvent, distanceX, distanceY);
}
if (!isMovingInPopup) {
animateView(playerImpl.getCloseOverlayButton(), true, 200);
}
isMovingInPopup = true;
final float diffX = (int) (movingEvent.getRawX() - initialEvent.getRawX());
float posX = (int) (initialPopupX + diffX);
final float diffY = (int) (movingEvent.getRawY() - initialEvent.getRawY());
float posY = (int) (initialPopupY + diffY);
if (posX > (playerImpl.getScreenWidth() - playerImpl.getPopupWidth())) {
posX = (int) (playerImpl.getScreenWidth() - playerImpl.getPopupWidth());
} else if (posX < 0) {
posX = 0;
}
if (posY > (playerImpl.getScreenHeight() - playerImpl.getPopupHeight())) {
posY = (int) (playerImpl.getScreenHeight() - playerImpl.getPopupHeight());
} else if (posY < 0) {
posY = 0;
}
playerImpl.getPopupLayoutParams().x = (int) posX;
playerImpl.getPopupLayoutParams().y = (int) posY;
final View closingOverlayView = playerImpl.getClosingOverlayView();
if (playerImpl.isInsideClosingRadius(movingEvent)) {
if (closingOverlayView.getVisibility() == View.GONE) {
animateView(closingOverlayView, true, 250);
}
} else {
if (closingOverlayView.getVisibility() == View.VISIBLE) {
animateView(closingOverlayView, false, 0);
}
}
// if (DEBUG) {
// Log.d(TAG, "onScrollInPopup = "
// + "e1.getRaw = [" + initialEvent.getRawX() + ", "
// + initialEvent.getRawY() + "], "
// + "e1.getX,Y = [" + initialEvent.getX() + ", "
// + initialEvent.getY() + "], "
// + "e2.getRaw = [" + movingEvent.getRawX() + ", "
// + movingEvent.getRawY() + "], "
// + "e2.getX,Y = [" + movingEvent.getX() + ", " + movingEvent.getY() + "], "
// + "distanceX,Y = [" + distanceX + ", " + distanceY + "], "
// + "posX,Y = [" + posX + ", " + posY + "], "
// + "popupW,H = [" + popupWidth + " x " + popupHeight + "]");
// }
playerImpl.windowManager
.updateViewLayout(playerImpl.getRootView(), playerImpl.getPopupLayoutParams());
return true;
}
private void onScrollEndInPopup(final MotionEvent event) {
if (playerImpl == null) {
return;
}
if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) {
playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
}
if (playerImpl.isInsideClosingRadius(event)) {
playerImpl.closePopup();
} else {
animateView(playerImpl.getClosingOverlayView(), false, 0);
if (!playerImpl.isPopupClosing) {
animateView(playerImpl.getCloseOverlayButton(), false, 200);
}
}
}
private boolean onFlingInPopup(final MotionEvent e1,
final MotionEvent e2,
final float velocityX,
final float velocityY) {
if (playerImpl == null) {
return false;
}
final float absVelocityX = Math.abs(velocityX);
final float absVelocityY = Math.abs(velocityY);
if (Math.max(absVelocityX, absVelocityY) > tossFlingVelocity) {
if (absVelocityX > tossFlingVelocity) {
playerImpl.getPopupLayoutParams().x = (int) velocityX;
}
if (absVelocityY > tossFlingVelocity) {
playerImpl.getPopupLayoutParams().y = (int) velocityY;
}
playerImpl.checkPopupPositionBounds();
playerImpl.windowManager
.updateViewLayout(playerImpl.getRootView(), playerImpl.getPopupLayoutParams());
return true;
}
return false;
}
private boolean onTouchInPopup(final View v, final MotionEvent event) {
if (playerImpl == null) {
return false;
}
playerImpl.getGestureDetector().onTouchEvent(event);
if (event.getPointerCount() == 2 && !isMovingInPopup && !isResizing) {
if (DEBUG) {
Log.d(TAG, "onTouch() 2 finger pointer detected, enabling resizing.");
}
playerImpl.showAndAnimateControl(-1, true);
playerImpl.getLoadingPanel().setVisibility(View.GONE);
playerImpl.hideControls(0, 0);
animateView(playerImpl.getCurrentDisplaySeek(), false, 0, 0);
animateView(playerImpl.getResizingIndicator(), true, 200, 0);
//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 = Math.hypot(initFirstPointerX - initSecPointerX,
initFirstPointerY - initSecPointerY);
isResizing = true;
}
if (event.getAction() == MotionEvent.ACTION_MOVE && !isMovingInPopup && isResizing) {
if (DEBUG) {
Log.d(TAG, "onTouch() ACTION_MOVE > v = [" + v + "], "
+ "e1.getRaw = [" + event.getRawX() + ", " + event.getRawY() + "]");
}
return handleMultiDrag(event);
}
if (event.getAction() == MotionEvent.ACTION_UP) {
if (DEBUG) {
Log.d(TAG, "onTouch() ACTION_UP > v = [" + v + "], "
+ "e1.getRaw = [" + event.getRawX() + ", " + event.getRawY() + "]");
}
if (isMovingInPopup) {
isMovingInPopup = false;
onScrollEndInPopup(event);
}
if (isResizing) {
isResizing = false;
initPointerDistance = -1;
initFirstPointerX = -1;
initFirstPointerY = -1;
initSecPointerX = -1;
initSecPointerY = -1;
animateView(playerImpl.getResizingIndicator(), false, 100, 0);
playerImpl.changeState(playerImpl.getCurrentState());
}
if (!playerImpl.isPopupClosing) {
playerImpl.savePositionAndSize();
}
}
v.performClick();
return true;
}
private boolean handleMultiDrag(final MotionEvent event) {
if (initPointerDistance != -1 && event.getPointerCount() == 2) {
// get the movements of the fingers
double firstPointerMove = Math.hypot(event.getX(0) - initFirstPointerX,
event.getY(0) - initFirstPointerY);
double secPointerMove = Math.hypot(event.getX(1) - initSecPointerX,
event.getY(1) - initSecPointerY);
// minimum threshold beyond which pinch gesture will work
int minimumMove = ViewConfiguration.get(service).getScaledTouchSlop();
if (Math.max(firstPointerMove, secPointerMove) > minimumMove) {
// calculate current distance between the pointers
final double currentPointerDistance =
Math.hypot(event.getX(0) - event.getX(1),
event.getY(0) - event.getY(1));
double popupWidth = playerImpl.getPopupWidth();
// change co-ordinates of popup so the center stays at the same position
double newWidth = (popupWidth * currentPointerDistance / initPointerDistance);
initPointerDistance = currentPointerDistance;
playerImpl.getPopupLayoutParams().x += (popupWidth - newWidth) / 2;
playerImpl.checkPopupPositionBounds();
playerImpl.updateScreenSize();
playerImpl.updatePopupSize(
(int) Math.min(playerImpl.getScreenWidth(), newWidth),
-1);
return true;
}
}
return false;
}
/*
* Utils
* */
private int getNavigationBarHeight(final Context context) {
int resId = context.getResources()
.getIdentifier("navigation_bar_height", "dimen", "android");
if (resId > 0) {
return context.getResources().getDimensionPixelSize(resId);
}
return 0;
}
private int getStatusBarHeight(final Context context) {
int resId = context.getResources()
.getIdentifier("status_bar_height", "dimen", "android");
if (resId > 0) {
return context.getResources().getDimensionPixelSize(resId);
}
return 0;
}
}

View file

@ -0,0 +1,15 @@
package org.schabi.newpipe.player.event;
import com.google.android.exoplayer2.ExoPlaybackException;
public interface PlayerServiceEventListener extends PlayerEventListener {
void onFullscreenStateChanged(boolean fullscreen);
void onScreenRotationButtonClicked();
void onMoreOptionsLongClicked();
void onPlayerError(ExoPlaybackException error);
void hideSystemUiIfNeeded();
}

View file

@ -114,7 +114,7 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, An
private void onAudioFocusGain() {
Log.d(TAG, "onAudioFocusGain() called");
player.setVolume(DUCK_AUDIO_TO);
animateAudio(DUCK_AUDIO_TO, 1f);
animateAudio(DUCK_AUDIO_TO, 1.0f);
if (PlayerHelper.isResumeAfterAudioFocusGain(context)) {
player.setPlayWhenReady(true);

View file

@ -92,8 +92,10 @@ public class PlaybackParameterDialog extends DialogFragment {
public static PlaybackParameterDialog newInstance(final double playbackTempo,
final double playbackPitch,
final boolean playbackSkipSilence) {
final boolean playbackSkipSilence,
final Callback callback) {
PlaybackParameterDialog dialog = new PlaybackParameterDialog();
dialog.callback = callback;
dialog.initialTempo = playbackTempo;
dialog.initialPitch = playbackPitch;
@ -111,9 +113,9 @@ public class PlaybackParameterDialog extends DialogFragment {
@Override
public void onAttach(final Context context) {
super.onAttach(context);
if (context != null && context instanceof Callback) {
if (context instanceof Callback) {
callback = (Callback) context;
} else {
} else if (callback == null) {
dismiss();
}
}

View file

@ -4,6 +4,7 @@ import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.preference.PreferenceManager;
import android.provider.Settings;
import android.view.accessibility.CaptioningManager;
import androidx.annotation.IntDef;
@ -45,6 +46,9 @@ import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MOD
import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT;
import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_ZOOM;
import static java.lang.annotation.RetentionPolicy.SOURCE;
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_ALWAYS;
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_WIFI;
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_NEVER;
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;
@ -56,6 +60,15 @@ public final class PlayerHelper {
private static final NumberFormat SPEED_FORMATTER = new DecimalFormat("0.##x");
private static final NumberFormat PITCH_FORMATTER = new DecimalFormat("##%");
@Retention(SOURCE)
@IntDef({AUTOPLAY_TYPE_ALWAYS, AUTOPLAY_TYPE_WIFI,
AUTOPLAY_TYPE_NEVER})
public @interface AutoplayType {
int AUTOPLAY_TYPE_ALWAYS = 0;
int AUTOPLAY_TYPE_WIFI = 1;
int AUTOPLAY_TYPE_NEVER = 2;
}
private PlayerHelper() { }
////////////////////////////////////////////////////////////////////////////
@ -203,6 +216,11 @@ public final class PlayerHelper {
return isAutoQueueEnabled(context, false);
}
public static boolean isClearingQueueConfirmationRequired(@NonNull final Context context) {
return getPreferences(context)
.getBoolean(context.getString(R.string.clear_queue_confirmation_key), false);
}
@MinimizeMode
public static int getMinimizeOnExitAction(@NonNull final Context context) {
final String defaultAction = context.getString(R.string.minimize_on_exit_none_key);
@ -219,6 +237,18 @@ public final class PlayerHelper {
}
}
@AutoplayType
public static int getAutoplayType(@NonNull final Context context) {
final String type = getAutoplayType(context, context.getString(R.string.autoplay_wifi_key));
if (type.equals(context.getString(R.string.autoplay_always_key))) {
return AUTOPLAY_TYPE_ALWAYS;
} else if (type.equals(context.getString(R.string.autoplay_never_key))) {
return AUTOPLAY_TYPE_NEVER;
} else {
return AUTOPLAY_TYPE_WIFI;
}
}
@NonNull
public static SeekParameters getSeekParameters(@NonNull final Context context) {
return isUsingInexactSeek(context) ? SeekParameters.CLOSEST_SYNC : SeekParameters.EXACT;
@ -308,7 +338,7 @@ public final class PlayerHelper {
final CaptioningManager captioningManager
= (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE);
if (captioningManager == null || !captioningManager.isEnabled()) {
return 1f;
return 1.0f;
}
return captioningManager.getFontScale();
@ -324,6 +354,13 @@ public final class PlayerHelper {
setScreenBrightness(context, setScreenBrightness, System.currentTimeMillis());
}
public static boolean globalScreenOrientationLocked(final Context context) {
// 1: Screen orientation changes using accelerometer
// 0: Screen orientation is locked
return android.provider.Settings.System.getInt(
context.getContentResolver(), Settings.System.ACCELEROMETER_ROTATION, 0) == 0;
}
////////////////////////////////////////////////////////////////////////////
// Private helpers
////////////////////////////////////////////////////////////////////////////
@ -396,6 +433,12 @@ public final class PlayerHelper {
.getString(context.getString(R.string.minimize_on_exit_key), key);
}
private static String getAutoplayType(@NonNull final Context context,
final String key) {
return getPreferences(context).getString(context.getString(R.string.autoplay_key),
key);
}
private static SinglePlayQueue getAutoQueuedSinglePlayQueue(
final StreamInfoItem streamInfoItem) {
SinglePlayQueue singlePlayQueue = new SinglePlayQueue(streamInfoItem);

View file

@ -51,16 +51,24 @@ public abstract class PlayQueue implements Serializable {
@NonNull
private final AtomicInteger queueIndex;
private final ArrayList<PlayQueueItem> history;
private transient BehaviorSubject<PlayQueueEvent> eventBroadcast;
private transient Flowable<PlayQueueEvent> broadcastReceiver;
private transient Subscription reportingReactor;
private transient boolean disposed;
PlayQueue(final int index, final List<PlayQueueItem> startWith) {
streams = new ArrayList<>();
streams.addAll(startWith);
history = new ArrayList<>();
if (streams.size() > index) {
history.add(streams.get(index));
}
queueIndex = new AtomicInteger(index);
disposed = false;
}
/*//////////////////////////////////////////////////////////////////////////
@ -99,6 +107,7 @@ public abstract class PlayQueue implements Serializable {
eventBroadcast = null;
broadcastReceiver = null;
reportingReactor = null;
disposed = true;
}
/**
@ -149,6 +158,9 @@ public abstract class PlayQueue implements Serializable {
if (index >= streams.size()) {
newIndex = isComplete() ? index % streams.size() : streams.size() - 1;
}
if (oldIndex != newIndex) {
history.add(streams.get(newIndex));
}
queueIndex.set(newIndex);
broadcast(new SelectEvent(oldIndex, newIndex));
@ -269,7 +281,7 @@ public abstract class PlayQueue implements Serializable {
* @param items {@link PlayQueueItem}s to append
*/
public synchronized void append(@NonNull final List<PlayQueueItem> items) {
List<PlayQueueItem> itemList = new ArrayList<>(items);
final List<PlayQueueItem> itemList = new ArrayList<>(items);
if (isShuffled()) {
backup.addAll(itemList);
@ -314,6 +326,9 @@ public abstract class PlayQueue implements Serializable {
public synchronized void error() {
final int oldIndex = getIndex();
queueIndex.incrementAndGet();
if (streams.size() > queueIndex.get()) {
history.add(streams.get(queueIndex.get()));
}
broadcast(new ErrorEvent(oldIndex, getIndex()));
}
@ -334,7 +349,11 @@ public abstract class PlayQueue implements Serializable {
if (backup != null) {
backup.remove(getItem(removeIndex));
}
streams.remove(removeIndex);
history.remove(streams.remove(removeIndex));
if (streams.size() > queueIndex.get()) {
history.add(streams.get(queueIndex.get()));
}
}
/**
@ -367,7 +386,7 @@ public abstract class PlayQueue implements Serializable {
queueIndex.incrementAndGet();
}
PlayQueueItem playQueueItem = streams.remove(source);
final PlayQueueItem playQueueItem = streams.remove(source);
playQueueItem.setAutoQueued(false);
streams.add(target, playQueueItem);
broadcast(new MoveEvent(source, target));
@ -427,6 +446,9 @@ public abstract class PlayQueue implements Serializable {
streams.add(0, streams.remove(newIndex));
}
queueIndex.set(0);
if (streams.size() > 0) {
history.add(streams.get(0));
}
broadcast(new ReorderEvent(originIndex, queueIndex.get()));
}
@ -458,10 +480,60 @@ public abstract class PlayQueue implements Serializable {
} else {
queueIndex.set(0);
}
if (streams.size() > queueIndex.get()) {
history.add(streams.get(queueIndex.get()));
}
broadcast(new ReorderEvent(originIndex, queueIndex.get()));
}
/**
* Selects previous played item.
*
* This method removes currently playing item from history and
* starts playing the last item from history if it exists
*
* @return true if history is not empty and the item can be played
* */
public synchronized boolean previous() {
if (history.size() <= 1) {
return false;
}
history.remove(history.size() - 1);
final PlayQueueItem last = history.remove(history.size() - 1);
setIndex(indexOf(last));
return true;
}
/*
* Compares two PlayQueues. Useful when a user switches players but queue is the same so
* we don't have to do anything with new queue.
* This method also gives a chance to track history of items in a queue in
* VideoDetailFragment without duplicating items from two identical queues
* */
@Override
public boolean equals(@Nullable final Object obj) {
if (!(obj instanceof PlayQueue)
|| getStreams().size() != ((PlayQueue) obj).getStreams().size()) {
return false;
}
final PlayQueue other = (PlayQueue) obj;
for (int i = 0; i < getStreams().size(); i++) {
if (!getItem(i).getUrl().equals(other.getItem(i).getUrl())) {
return false;
}
}
return true;
}
public boolean isDisposed() {
return disposed;
}
/*//////////////////////////////////////////////////////////////////////////
// Rx Broadcast
//////////////////////////////////////////////////////////////////////////*/