1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2025-02-02 04:09:13 +00:00

Merge pull request #11867 from Profpatsch/player-holder-refactor

PlayerHolder refactor
This commit is contained in:
Profpatsch 2025-01-27 13:29:53 +01:00 committed by GitHub
commit b109e4d3cc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 143 additions and 99 deletions

View File

@ -95,7 +95,8 @@ import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.PlayerService; import org.schabi.newpipe.player.PlayerService;
import org.schabi.newpipe.player.PlayerType; import org.schabi.newpipe.player.PlayerType;
import org.schabi.newpipe.player.event.OnKeyDownListener; import org.schabi.newpipe.player.event.OnKeyDownListener;
import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener; import org.schabi.newpipe.player.event.PlayerHolderLifecycleEventListener;
import org.schabi.newpipe.player.event.PlayerServiceEventListener;
import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.player.helper.PlayerHolder; import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
@ -136,7 +137,8 @@ import io.reactivex.rxjava3.schedulers.Schedulers;
public final class VideoDetailFragment public final class VideoDetailFragment
extends BaseStateFragment<StreamInfo> extends BaseStateFragment<StreamInfo>
implements BackPressable, implements BackPressable,
PlayerServiceExtendedEventListener, PlayerServiceEventListener,
PlayerHolderLifecycleEventListener,
OnKeyDownListener { OnKeyDownListener {
public static final String KEY_SWITCHING_PLAYERS = "switching_players"; public static final String KEY_SWITCHING_PLAYERS = "switching_players";
@ -234,10 +236,9 @@ public final class VideoDetailFragment
// Service management // Service management
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@Override @Override
public void onServiceConnected(final Player connectedPlayer, public void onServiceConnected(final PlayerService connectedPlayerService,
final PlayerService connectedPlayerService,
final boolean playAfterConnect) { final boolean playAfterConnect) {
player = connectedPlayer; player = connectedPlayerService.getPlayer();
playerService = connectedPlayerService; playerService = connectedPlayerService;
// It will do nothing if the player is not in fullscreen mode // It will do nothing if the player is not in fullscreen mode
@ -393,7 +394,7 @@ public final class VideoDetailFragment
if (activity.isFinishing() && isPlayerAvailable() && player.videoPlayerSelected()) { if (activity.isFinishing() && isPlayerAvailable() && player.videoPlayerSelected()) {
playerHolder.stopService(); playerHolder.stopService();
} else { } else {
playerHolder.setListener(null); playerHolder.unsetListeners();
} }
PreferenceManager.getDefaultSharedPreferences(activity) PreferenceManager.getDefaultSharedPreferences(activity)
@ -658,10 +659,10 @@ public final class VideoDetailFragment
}); });
setupBottomPlayer(); setupBottomPlayer();
if (!playerHolder.isBound()) { if (playerHolder.isNotBoundYet()) {
setHeightThumbnail(); setHeightThumbnail();
} else { } else {
playerHolder.startService(false, this); playerHolder.startService(false, this, this);
} }
} }
@ -1052,7 +1053,7 @@ public final class VideoDetailFragment
// See UI changes while remote playQueue changes // See UI changes while remote playQueue changes
if (!isPlayerAvailable()) { if (!isPlayerAvailable()) {
playerHolder.startService(false, this); playerHolder.startService(false, this, this);
} else { } else {
// FIXME Workaround #7427 // FIXME Workaround #7427
player.setRecovery(); player.setRecovery();
@ -1115,7 +1116,7 @@ public final class VideoDetailFragment
private void openNormalBackgroundPlayer(final boolean append) { private void openNormalBackgroundPlayer(final boolean append) {
// See UI changes while remote playQueue changes // See UI changes while remote playQueue changes
if (!isPlayerAvailable()) { if (!isPlayerAvailable()) {
playerHolder.startService(false, this); playerHolder.startService(false, this, this);
} }
final PlayQueue queue = setupPlayQueueForIntent(append); final PlayQueue queue = setupPlayQueueForIntent(append);
@ -1129,7 +1130,7 @@ public final class VideoDetailFragment
private void openMainPlayer() { private void openMainPlayer() {
if (!isPlayerServiceAvailable()) { if (!isPlayerServiceAvailable()) {
playerHolder.startService(autoPlayEnabled, this); playerHolder.startService(autoPlayEnabled, this, this);
return; return;
} }
if (currentInfo == null) { if (currentInfo == null) {
@ -1384,9 +1385,11 @@ public final class VideoDetailFragment
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
} }
// Rebound to the service if it was closed via notification or mini player // Rebound to the service if it was closed via notification or mini player
if (!playerHolder.isBound()) { if (playerHolder.isNotBoundYet()) {
playerHolder.startService( playerHolder.startService(
false, VideoDetailFragment.this); false,
VideoDetailFragment.this,
VideoDetailFragment.this);
} }
break; break;
} }

View File

@ -217,11 +217,16 @@ public final class PlayQueueActivity extends AppCompatActivity
} }
@Override @Override
public void onServiceConnected(final ComponentName name, final IBinder service) { public void onServiceConnected(final ComponentName name, final IBinder binder) {
Log.d(TAG, "Player service is connected"); Log.d(TAG, "Player service is connected");
if (service instanceof PlayerService.LocalBinder) { if (binder instanceof PlayerService.LocalBinder localBinder) {
player = ((PlayerService.LocalBinder) service).getPlayer(); final @Nullable PlayerService s = localBinder.getService();
if (s == null) {
player = null;
} else {
player = s.getPlayer();
}
} }
if (player == null || player.getPlayQueue() == null || player.exoPlayerIsNull()) { if (player == null || player.getPlayQueue() == null || player.exoPlayerIsNull()) {

View File

@ -28,6 +28,8 @@ import android.os.Binder;
import android.os.IBinder; import android.os.IBinder;
import android.util.Log; import android.util.Log;
import androidx.annotation.Nullable;
import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi; import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi;
import org.schabi.newpipe.player.notification.NotificationPlayerUi; import org.schabi.newpipe.player.notification.NotificationPlayerUi;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
@ -36,7 +38,9 @@ import java.lang.ref.WeakReference;
/** /**
* One service for all players. * One background service for our player. Even though the player has multiple UIs
* (e.g. the audio-only UI, the main UI, the pulldown-menu UI),
* this allows us to keep playing even when switching between the different UIs.
*/ */
public final class PlayerService extends Service { public final class PlayerService extends Service {
private static final String TAG = PlayerService.class.getSimpleName(); private static final String TAG = PlayerService.class.getSimpleName();
@ -46,6 +50,9 @@ public final class PlayerService extends Service {
private final IBinder mBinder = new PlayerService.LocalBinder(this); private final IBinder mBinder = new PlayerService.LocalBinder(this);
public Player getPlayer() {
return player;
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Service's LifeCycle // Service's LifeCycle
@ -167,6 +174,10 @@ public final class PlayerService extends Service {
return mBinder; return mBinder;
} }
/**
* Allows us this {@link org.schabi.newpipe.player.PlayerService} over the Service boundary
* back to our {@link org.schabi.newpipe.player.helper.PlayerHolder}.
*/
public static class LocalBinder extends Binder { public static class LocalBinder extends Binder {
private final WeakReference<PlayerService> playerService; private final WeakReference<PlayerService> playerService;
@ -174,12 +185,12 @@ public final class PlayerService extends Service {
this.playerService = new WeakReference<>(playerService); this.playerService = new WeakReference<>(playerService);
} }
public PlayerService getService() { /**
* Get the PlayerService object itself.
* @return this
* */
public @Nullable PlayerService getService() {
return playerService.get(); return playerService.get();
} }
public Player getPlayer() {
return playerService.get().player;
}
} }
} }

View File

@ -5,6 +5,7 @@ import com.google.android.exoplayer2.PlaybackParameters;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
/** Player-specific events like queue or progress updates. */
public interface PlayerEventListener { public interface PlayerEventListener {
void onQueueUpdate(PlayQueue queue); void onQueueUpdate(PlayQueue queue);
void onPlaybackUpdate(int state, int repeatMode, boolean shuffled, void onPlaybackUpdate(int state, int repeatMode, boolean shuffled,

View File

@ -0,0 +1,10 @@
package org.schabi.newpipe.player.event;
import org.schabi.newpipe.player.PlayerService;
/** Gets signalled if our PlayerHolder (dis)connects from the PlayerService. */
public interface PlayerHolderLifecycleEventListener {
void onServiceConnected(PlayerService playerService,
boolean playAfterConnect);
void onServiceDisconnected();
}

View File

@ -2,6 +2,9 @@ package org.schabi.newpipe.player.event;
import com.google.android.exoplayer2.PlaybackException; import com.google.android.exoplayer2.PlaybackException;
/** {@link org.schabi.newpipe.player.event.PlayerEventListener} that also gets called for
* application-specific events like screen rotation or UI changes.
*/
public interface PlayerServiceEventListener extends PlayerEventListener { public interface PlayerServiceEventListener extends PlayerEventListener {
void onViewCreated(); void onViewCreated();

View File

@ -1,11 +0,0 @@
package org.schabi.newpipe.player.event;
import org.schabi.newpipe.player.PlayerService;
import org.schabi.newpipe.player.Player;
public interface PlayerServiceExtendedEventListener extends PlayerServiceEventListener {
void onServiceConnected(Player player,
PlayerService playerService,
boolean playAfterConnect);
void onServiceDisconnected();
}

View File

@ -7,6 +7,7 @@ import android.content.ServiceConnection;
import android.os.IBinder; import android.os.IBinder;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
@ -20,9 +21,13 @@ import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.PlayerService; import org.schabi.newpipe.player.PlayerService;
import org.schabi.newpipe.player.PlayerType; import org.schabi.newpipe.player.PlayerType;
import org.schabi.newpipe.player.event.PlayerServiceEventListener; import org.schabi.newpipe.player.event.PlayerServiceEventListener;
import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener; import org.schabi.newpipe.player.event.PlayerHolderLifecycleEventListener;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
/**
* Singleton that manages a `PlayerService`
* and can be used to control the player instance through the service.
*/
public final class PlayerHolder { public final class PlayerHolder {
private PlayerHolder() { private PlayerHolder() {
@ -39,10 +44,12 @@ public final class PlayerHolder {
private static final boolean DEBUG = MainActivity.DEBUG; private static final boolean DEBUG = MainActivity.DEBUG;
private static final String TAG = PlayerHolder.class.getSimpleName(); private static final String TAG = PlayerHolder.class.getSimpleName();
@Nullable private PlayerServiceExtendedEventListener listener; @Nullable private PlayerServiceEventListener listener;
@Nullable private PlayerHolderLifecycleEventListener holderListener;
private final PlayerServiceConnection serviceConnection = new PlayerServiceConnection(); private final PlayerServiceConnection serviceConnection = new PlayerServiceConnection();
private boolean bound; private boolean bound;
@Nullable private PlayerService playerService; @Nullable private PlayerService playerService;
@Nullable private Player player; @Nullable private Player player;
@ -80,8 +87,8 @@ public final class PlayerHolder {
return player != null && player.getPlayQueue() != null; return player != null && player.getPlayQueue() != null;
} }
public boolean isBound() { public boolean isNotBoundYet() {
return bound; return !bound;
} }
public int getQueueSize() { public int getQueueSize() {
@ -99,30 +106,48 @@ public final class PlayerHolder {
return player.getPlayQueue().getIndex(); return player.getPlayQueue().getIndex();
} }
public void setListener(@Nullable final PlayerServiceExtendedEventListener newListener) { public void unsetListeners() {
listener = newListener; listener = null;
holderListener = null;
if (listener == null) {
return;
} }
public void setListener(@NonNull final PlayerServiceEventListener newListener,
@NonNull final PlayerHolderLifecycleEventListener newHolderListener) {
listener = newListener;
holderListener = newHolderListener;
// Force reload data from service // Force reload data from service
if (player != null) { if (player != null) {
listener.onServiceConnected(player, playerService, false); holderListener.onServiceConnected(playerService, false);
startPlayerListener(); player.setFragmentListener(internalListener);
} }
} }
// helper to handle context in common place as using the same /**
// context to bind/unbind a service is crucial * Helper to handle context in common place as using the same
* context to bind/unbind a service is crucial.
*
* @return the common context
* */
private Context getCommonContext() { private Context getCommonContext() {
return App.getInstance(); return App.getInstance();
} }
/**
* Connect to (and if needed start) the {@link PlayerService}
* and bind {@link PlayerServiceConnection} to it.
* If the service is already started, only set the listener.
* @param playAfterConnect If the service is started, start playing immediately
* @param newListener set this listener
* @param newHolderListener set this listener
* */
public void startService(final boolean playAfterConnect, public void startService(final boolean playAfterConnect,
final PlayerServiceExtendedEventListener newListener) { final PlayerServiceEventListener newListener,
final PlayerHolderLifecycleEventListener newHolderListener
) {
final Context context = getCommonContext(); final Context context = getCommonContext();
setListener(newListener); setListener(newListener, newHolderListener);
if (bound) { if (bound) {
return; return;
} }
@ -131,8 +156,18 @@ public final class PlayerHolder {
// bound twice. Prevent it with unbinding first // bound twice. Prevent it with unbinding first
unbind(context); unbind(context);
ContextCompat.startForegroundService(context, new Intent(context, PlayerService.class)); ContextCompat.startForegroundService(context, new Intent(context, PlayerService.class));
serviceConnection.doPlayAfterConnect(playAfterConnect); serviceConnection.playAfterConnect = playAfterConnect;
bind(context);
if (DEBUG) {
Log.d(TAG, "bind() called");
}
final Intent serviceIntent = new Intent(context, PlayerService.class);
bound = context.bindService(serviceIntent, serviceConnection,
Context.BIND_AUTO_CREATE);
if (!bound) {
context.unbindService(serviceConnection);
}
} }
public void stopService() { public void stopService() {
@ -141,14 +176,35 @@ public final class PlayerHolder {
context.stopService(new Intent(context, PlayerService.class)); context.stopService(new Intent(context, PlayerService.class));
} }
/**
* Call {@link Context#unbindService(ServiceConnection)} on our service
* (does not necesarily stop the service right away).
* Remove all our listeners and deinitialize them.
* @param context shared context
* */
private void unbind(final Context context) {
if (DEBUG) {
Log.d(TAG, "unbind() called");
}
if (bound) {
context.unbindService(serviceConnection);
bound = false;
if (player != null) {
player.removeFragmentListener(internalListener);
}
playerService = null;
player = null;
if (holderListener != null) {
holderListener.onServiceDisconnected();
}
}
}
class PlayerServiceConnection implements ServiceConnection { class PlayerServiceConnection implements ServiceConnection {
private boolean playAfterConnect = false; private boolean playAfterConnect = false;
public void doPlayAfterConnect(final boolean playAfterConnection) {
this.playAfterConnect = playAfterConnection;
}
@Override @Override
public void onServiceDisconnected(final ComponentName compName) { public void onServiceDisconnected(final ComponentName compName) {
if (DEBUG) { if (DEBUG) {
@ -167,56 +223,22 @@ public final class PlayerHolder {
final PlayerService.LocalBinder localBinder = (PlayerService.LocalBinder) service; final PlayerService.LocalBinder localBinder = (PlayerService.LocalBinder) service;
playerService = localBinder.getService(); playerService = localBinder.getService();
player = localBinder.getPlayer(); player = playerService != null ? playerService.getPlayer() : null;
if (listener != null) {
listener.onServiceConnected(player, playerService, playAfterConnect);
}
startPlayerListener();
}
}
private void bind(final Context context) { if (holderListener != null) {
if (DEBUG) { holderListener.onServiceConnected(playerService, playAfterConnect);
Log.d(TAG, "bind() called");
} }
final Intent serviceIntent = new Intent(context, PlayerService.class);
bound = context.bindService(serviceIntent, serviceConnection,
Context.BIND_AUTO_CREATE);
if (!bound) {
context.unbindService(serviceConnection);
}
}
private void unbind(final Context context) {
if (DEBUG) {
Log.d(TAG, "unbind() called");
}
if (bound) {
context.unbindService(serviceConnection);
bound = false;
stopPlayerListener();
playerService = null;
player = null;
if (listener != null) {
listener.onServiceDisconnected();
}
}
}
private void startPlayerListener() {
if (player != null) { if (player != null) {
player.setFragmentListener(internalListener); player.setFragmentListener(internalListener);
} }
} }
private void stopPlayerListener() {
if (player != null) {
player.removeFragmentListener(internalListener);
}
} }
/**
* Delegate all {@link PlayerServiceEventListener} events to our current `listener` object.
* Only difference is that if {@link PlayerServiceEventListener#onServiceStopped()} is called,
* it also calls {@link PlayerHolder#unbind(Context)}.
* */
private final PlayerServiceEventListener internalListener = private final PlayerServiceEventListener internalListener =
new PlayerServiceEventListener() { new PlayerServiceEventListener() {
@Override @Override