1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2026-02-06 18:20:16 +00:00

Compare commits

...

10 Commits

Author SHA1 Message Date
Profpatsch
31ade7cd30 PlayerUIList: make UI list private 2025-05-13 16:00:24 +02:00
Profpatsch
4e1b0e0555 Player: destroy -> saveAndShutdown 2025-05-13 16:00:24 +02:00
Profpatsch
0aa71a58ed PlayerHolder: improve interface docstrings 2025-05-13 16:00:24 +02:00
Profpatsch
7585cc2e73 VideoPlayerUi: suppress warnings
The `R.id` link from the comment cannot be resolved, so let’s not link
it for now.

We are using some exoplayer2 resources, let’s silence the warning.
2025-05-13 16:00:24 +02:00
Profpatsch
803fd52859 VideoDetailFragment: remove duplicate code in startLoading 2025-05-13 15:59:26 +02:00
Profpatsch
ab8a9ae11c VideoDetailFragment: apply more IDE suggestions 2025-05-13 15:59:26 +02:00
Profpatsch
83486402df VideoDetailFragment: apply visibility suggestions
Because the class is final, protected does not make sense (Android
Studio auto-suggestions)
2025-05-13 15:59:26 +02:00
Profpatsch
a5813f256a PlayerService: simplify nullable calls, getters 2025-05-13 15:59:26 +02:00
Profpatsch
0a885492b6 PlayerService: Convert to kotlin (mechanical) 2025-05-13 15:58:31 +02:00
Profpatsch
731efc2124 PlayerUIList: restrict superclasses a little 2025-05-13 15:58:31 +02:00
6 changed files with 252 additions and 237 deletions

View File

@@ -188,21 +188,21 @@ public final class VideoDetailFragment
}; };
@State @State
protected int serviceId = Constants.NO_SERVICE_ID; int serviceId = Constants.NO_SERVICE_ID;
@State @State
@NonNull @NonNull
protected String title = ""; String title = "";
@State @State
@Nullable @Nullable
protected String url = null; String url = null;
@Nullable @Nullable
protected PlayQueue playQueue = null; private PlayQueue playQueue = null;
@State @State
int bottomSheetState = BottomSheetBehavior.STATE_EXPANDED; int bottomSheetState = BottomSheetBehavior.STATE_EXPANDED;
@State @State
int lastStableBottomSheetState = BottomSheetBehavior.STATE_EXPANDED; int lastStableBottomSheetState = BottomSheetBehavior.STATE_EXPANDED;
@State @State
protected boolean autoPlayEnabled = true; boolean autoPlayEnabled = true;
@Nullable @Nullable
private StreamInfo currentInfo = null; private StreamInfo currentInfo = null;
@@ -438,18 +438,15 @@ public final class VideoDetailFragment
@Override @Override
public void onActivityResult(final int requestCode, final int resultCode, final Intent data) { public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
super.onActivityResult(requestCode, resultCode, data); super.onActivityResult(requestCode, resultCode, data);
switch (requestCode) { if (requestCode == ReCaptchaActivity.RECAPTCHA_REQUEST) {
case ReCaptchaActivity.RECAPTCHA_REQUEST: if (resultCode == Activity.RESULT_OK) {
if (resultCode == Activity.RESULT_OK) { NavigationHelper.openVideoDetailFragment(requireContext(), getFM(),
NavigationHelper.openVideoDetailFragment(requireContext(), getFM(), serviceId, url, title, null, false);
serviceId, url, title, null, false); } else {
} else { Log.e(TAG, "ReCaptcha failed");
Log.e(TAG, "ReCaptcha failed"); }
} } else {
break; Log.e(TAG, "Request code from activity not supported [" + requestCode + "]");
default:
Log.e(TAG, "Request code from activity not supported [" + requestCode + "]");
break;
} }
} }
@@ -815,25 +812,17 @@ public final class VideoDetailFragment
} }
protected void prepareAndLoadInfo() { private void prepareAndLoadInfo() {
scrollToTop(); scrollToTop();
startLoading(false); startLoading(false);
} }
@Override @Override
public void startLoading(final boolean forceLoad) { public void startLoading(final boolean forceLoad) {
super.startLoading(forceLoad); startLoading(forceLoad, null);
initTabs();
currentInfo = null;
if (currentWorker != null) {
currentWorker.dispose();
}
runWorker(forceLoad, stack.isEmpty());
} }
private void startLoading(final boolean forceLoad, final boolean addToBackStack) { private void startLoading(final boolean forceLoad, final @Nullable Boolean addToBackStack) {
super.startLoading(forceLoad); super.startLoading(forceLoad);
initTabs(); initTabs();
@@ -842,7 +831,7 @@ public final class VideoDetailFragment
currentWorker.dispose(); currentWorker.dispose();
} }
runWorker(forceLoad, addToBackStack); runWorker(forceLoad, addToBackStack != null ? addToBackStack : stack.isEmpty());
} }
private void runWorker(final boolean forceLoad, final boolean addToBackStack) { private void runWorker(final boolean forceLoad, final boolean addToBackStack) {
@@ -1138,7 +1127,7 @@ public final class VideoDetailFragment
} }
private void openMainPlayer() { private void openMainPlayer() {
if (!isPlayerServiceAvailable()) { if (noPlayerServiceAvailable()) {
playerHolder.startService(autoPlayEnabled, this); playerHolder.startService(autoPlayEnabled, this);
return; return;
} }
@@ -1163,7 +1152,7 @@ public final class VideoDetailFragment
*/ */
private void hideMainPlayerOnLoadingNewStream() { private void hideMainPlayerOnLoadingNewStream() {
final var root = getRoot(); final var root = getRoot();
if (!isPlayerServiceAvailable() || root.isEmpty() || !player.videoPlayerSelected()) { if (noPlayerServiceAvailable() || root.isEmpty() || !player.videoPlayerSelected()) {
return; return;
} }
@@ -1337,23 +1326,23 @@ public final class VideoDetailFragment
binding.detailContentRootHiding.setVisibility(View.VISIBLE); binding.detailContentRootHiding.setVisibility(View.VISIBLE);
} }
protected void setInitialData(final int newServiceId, private void setInitialData(final int newServiceId,
@Nullable final String newUrl, @Nullable final String newUrl,
@NonNull final String newTitle, @NonNull final String newTitle,
@Nullable final PlayQueue newPlayQueue) { @Nullable final PlayQueue newPlayQueue) {
this.serviceId = newServiceId; this.serviceId = newServiceId;
this.url = newUrl; this.url = newUrl;
this.title = newTitle; this.title = newTitle;
this.playQueue = newPlayQueue; this.playQueue = newPlayQueue;
} }
private void setErrorImage(final int imageResource) { private void setErrorImage() {
if (binding == null || activity == null) { if (binding == null || activity == null) {
return; return;
} }
binding.detailThumbnailImageView.setImageDrawable( binding.detailThumbnailImageView.setImageDrawable(
AppCompatResources.getDrawable(requireContext(), imageResource)); AppCompatResources.getDrawable(requireContext(), R.drawable.not_available_monkey));
animate(binding.detailThumbnailImageView, false, 0, AnimationType.ALPHA, animate(binding.detailThumbnailImageView, false, 0, AnimationType.ALPHA,
0, () -> animate(binding.detailThumbnailImageView, true, 500)); 0, () -> animate(binding.detailThumbnailImageView, true, 500));
} }
@@ -1361,7 +1350,7 @@ public final class VideoDetailFragment
@Override @Override
public void handleError() { public void handleError() {
super.handleError(); super.handleError();
setErrorImage(R.drawable.not_available_monkey); setErrorImage();
if (binding.relatedItemsLayout != null) { // hide related streams for tablets if (binding.relatedItemsLayout != null) { // hide related streams for tablets
binding.relatedItemsLayout.setVisibility(View.INVISIBLE); binding.relatedItemsLayout.setVisibility(View.INVISIBLE);
@@ -1776,16 +1765,14 @@ public final class VideoDetailFragment
final PlaybackParameters parameters) { final PlaybackParameters parameters) {
setOverlayPlayPauseImage(player != null && player.isPlaying()); setOverlayPlayPauseImage(player != null && player.isPlaying());
switch (state) { if (state == Player.STATE_PLAYING) {
case Player.STATE_PLAYING: if (binding.positionView.getAlpha() != 1.0f
if (binding.positionView.getAlpha() != 1.0f && player.getPlayQueue() != null
&& player.getPlayQueue() != null && player.getPlayQueue().getItem() != null
&& player.getPlayQueue().getItem() != null && player.getPlayQueue().getItem().getUrl().equals(url)) {
&& player.getPlayQueue().getItem().getUrl().equals(url)) { animate(binding.positionView, true, 100);
animate(binding.positionView, true, 100); animate(binding.detailPositionView, true, 100);
animate(binding.detailPositionView, true, 100); }
}
break;
} }
} }
@@ -2444,8 +2431,8 @@ public final class VideoDetailFragment
return player != null; return player != null;
} }
boolean isPlayerServiceAvailable() { boolean noPlayerServiceAvailable() {
return playerService != null; return playerService == null;
} }
boolean isPlayerAndPlayerServiceAvailable() { boolean isPlayerAndPlayerServiceAvailable() {

View File

@@ -492,15 +492,15 @@ public final class Player implements PlaybackListener, Listener {
switch (playerType) { switch (playerType) {
case MAIN: case MAIN:
UIs.destroyAll(PopupPlayerUi.class); UIs.destroyAllOfType(PopupPlayerUi.class);
UIs.addAndPrepare(new MainPlayerUi(this, binding)); UIs.addAndPrepare(new MainPlayerUi(this, binding));
break; break;
case POPUP: case POPUP:
UIs.destroyAll(MainPlayerUi.class); UIs.destroyAllOfType(MainPlayerUi.class);
UIs.addAndPrepare(new PopupPlayerUi(this, binding)); UIs.addAndPrepare(new PopupPlayerUi(this, binding));
break; break;
case AUDIO: case AUDIO:
UIs.destroyAll(VideoPlayerUi.class); UIs.destroyAllOfType(VideoPlayerUi.class);
break; break;
} }
} }
@@ -591,9 +591,15 @@ public final class Player implements PlaybackListener, Listener {
} }
} }
public void destroy() {
/**
* Shut down this player.
* Saves the stream progress, sets recovery.
* Then destroys the player in all UIs and destroys the UIs as well.
*/
public void saveAndShutdown() {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "destroy() called"); Log.d(TAG, "saveAndShutdown() called");
} }
saveStreamProgressState(); saveStreamProgressState();
@@ -606,7 +612,7 @@ public final class Player implements PlaybackListener, Listener {
databaseUpdateDisposable.clear(); databaseUpdateDisposable.clear();
progressUpdateDisposable.set(null); progressUpdateDisposable.set(null);
UIs.destroyAll(Object.class); // destroy every UI: obviously every UI extends Object UIs.destroyAllOfType(null);
} }
public void setRecovery() { public void setRecovery() {
@@ -1995,6 +2001,10 @@ public final class Player implements PlaybackListener, Listener {
triggerProgressUpdate(); triggerProgressUpdate();
} }
/**
* Remove the listener, if it was set.
* @param listener listener to remove
* */
public void removeFragmentListener(final PlayerServiceEventListener listener) { public void removeFragmentListener(final PlayerServiceEventListener listener) {
if (fragmentListener == listener) { if (fragmentListener == listener) {
fragmentListener = null; fragmentListener = null;
@@ -2009,6 +2019,10 @@ public final class Player implements PlaybackListener, Listener {
triggerProgressUpdate(); triggerProgressUpdate();
} }
/**
* Remove the listener, if it was set.
* @param listener listener to remove
* */
void removeActivityListener(final PlayerEventListener listener) { void removeActivityListener(final PlayerEventListener listener) {
if (activityListener == listener) { if (activityListener == listener) {
activityListener = null; activityListener = null;

View File

@@ -16,103 +16,101 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.schabi.newpipe.player
package org.schabi.newpipe.player; import android.content.Context
import android.content.Intent
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; import android.os.Binder
import android.os.Bundle
import android.content.Context; import android.os.IBinder
import android.content.Intent; import android.support.v4.media.MediaBrowserCompat
import android.os.Binder; import android.support.v4.media.session.MediaSessionCompat
import android.os.Bundle; import android.util.Log
import android.os.IBinder; import androidx.core.app.ServiceCompat
import android.support.v4.media.MediaBrowserCompat; import androidx.media.MediaBrowserServiceCompat
import android.support.v4.media.session.MediaSessionCompat; import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
import android.util.Log; import org.schabi.newpipe.ktx.toDebugString
import org.schabi.newpipe.player.mediabrowser.MediaBrowserImpl
import androidx.annotation.NonNull; import org.schabi.newpipe.player.mediabrowser.MediaBrowserPlaybackPreparer
import androidx.annotation.Nullable; import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi
import androidx.core.app.ServiceCompat; import org.schabi.newpipe.player.notification.NotificationPlayerUi
import androidx.media.MediaBrowserServiceCompat; import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.ThemeHelper
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector; import java.lang.ref.WeakReference
import java.util.function.BiConsumer
import org.schabi.newpipe.ktx.BundleKt; import java.util.function.Consumer
import org.schabi.newpipe.player.mediabrowser.MediaBrowserImpl;
import org.schabi.newpipe.player.mediabrowser.MediaBrowserPlaybackPreparer;
import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi;
import org.schabi.newpipe.player.notification.NotificationPlayerUi;
import org.schabi.newpipe.util.ThemeHelper;
import java.lang.ref.WeakReference;
import java.util.List;
import java.util.function.Consumer;
/** /**
* One service for all players. * One service for all players.
*/ */
public final class PlayerService extends MediaBrowserServiceCompat { class PlayerService : MediaBrowserServiceCompat() {
private static final String TAG = PlayerService.class.getSimpleName();
private static final boolean DEBUG = Player.DEBUG;
public static final String SHOULD_START_FOREGROUND_EXTRA = "should_start_foreground_extra";
public static final String BIND_PLAYER_HOLDER_ACTION = "bind_player_holder_action";
// These objects are used to cleanly separate the Service implementation (in this file) and the // These objects are used to cleanly separate the Service implementation (in this file) and the
// media browser and playback preparer implementations. At the moment the playback preparer is // media browser and playback preparer implementations. At the moment the playback preparer is
// only used in conjunction with the media browser. // only used in conjunction with the media browser.
private MediaBrowserImpl mediaBrowserImpl; private var mediaBrowserImpl: MediaBrowserImpl? = null
private MediaBrowserPlaybackPreparer mediaBrowserPlaybackPreparer; private var mediaBrowserPlaybackPreparer: MediaBrowserPlaybackPreparer? = null
// these are instantiated in onCreate() as per // these are instantiated in onCreate() as per
// https://developer.android.com/training/cars/media#browser_workflow // https://developer.android.com/training/cars/media#browser_workflow
private MediaSessionCompat mediaSession; private var mediaSession: MediaSessionCompat? = null
private MediaSessionConnector sessionConnector; private var sessionConnector: MediaSessionConnector? = null
@Nullable
private Player player;
private final IBinder mBinder = new PlayerService.LocalBinder(this);
/** /**
* The parameter taken by this {@link Consumer} can be null to indicate the player is being * @return the current active player instance. May be null, since the player service can outlive
* the player e.g. to respond to Android Auto media browser queries.
*/
var player: Player? = null
private set
private val mBinder: IBinder = LocalBinder(this)
/**
* The parameter taken by this [Consumer] can be null to indicate the player is being
* stopped. * stopped.
*/ */
@Nullable private var onPlayerStartedOrStopped: Consumer<Player?>? = null
private Consumer<Player> onPlayerStartedOrStopped = null;
//region Service lifecycle //region Service lifecycle
@Override override fun onCreate() {
public void onCreate() { super.onCreate()
super.onCreate();
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "onCreate() called"); Log.d(TAG, "onCreate() called")
} }
assureCorrectAppLanguage(this); Localization.assureCorrectAppLanguage(this)
ThemeHelper.setTheme(this); ThemeHelper.setTheme(this)
mediaBrowserImpl = new MediaBrowserImpl(this, this::notifyChildrenChanged); mediaBrowserImpl = MediaBrowserImpl(
this,
Consumer { parentId: String? ->
this.notifyChildrenChanged(
parentId!!
)
}
)
// see https://developer.android.com/training/cars/media#browser_workflow // see https://developer.android.com/training/cars/media#browser_workflow
mediaSession = new MediaSessionCompat(this, "MediaSessionPlayerServ"); mediaSession = MediaSessionCompat(this, "MediaSessionPlayerServ")
setSessionToken(mediaSession.getSessionToken()); setSessionToken(mediaSession!!.sessionToken)
sessionConnector = new MediaSessionConnector(mediaSession); sessionConnector = MediaSessionConnector(mediaSession!!)
sessionConnector.setMetadataDeduplicationEnabled(true); sessionConnector!!.setMetadataDeduplicationEnabled(true)
mediaBrowserPlaybackPreparer = new MediaBrowserPlaybackPreparer( mediaBrowserPlaybackPreparer = MediaBrowserPlaybackPreparer(
this, this,
sessionConnector::setCustomErrorMessage, BiConsumer { message: String?, code: Int? ->
() -> sessionConnector.setCustomErrorMessage(null), sessionConnector!!.setCustomErrorMessage(
(playWhenReady) -> { message,
if (player != null) { code!!
player.onPrepare(); )
} },
Runnable { sessionConnector!!.setCustomErrorMessage(null) },
Consumer { playWhenReady: Boolean? ->
if (player != null) {
player!!.onPrepare()
} }
); }
sessionConnector.setPlaybackPreparer(mediaBrowserPlaybackPreparer); )
sessionConnector!!.setPlaybackPreparer(mediaBrowserPlaybackPreparer)
// Note: you might be tempted to create the player instance and call startForeground here, // Note: you might be tempted to create the player instance and call startForeground here,
// but be aware that the Android system might start the service just to perform media // but be aware that the Android system might start the service just to perform media
@@ -123,22 +121,26 @@ public final class PlayerService extends MediaBrowserServiceCompat {
// useless notification. // useless notification.
} }
@Override override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
public int onStartCommand(final Intent intent, final int flags, final int startId) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "onStartCommand() called with: intent = [" + intent Log.d(
+ "], extras = [" + BundleKt.toDebugString(intent.getExtras()) TAG,
+ "], flags = [" + flags + "], startId = [" + startId + "]"); (
"onStartCommand() called with: intent = [" + intent +
"], extras = [" + intent.extras.toDebugString() +
"], flags = [" + flags + "], startId = [" + startId + "]"
)
)
} }
// All internal NewPipe intents used to interact with the player, that are sent to the // All internal NewPipe intents used to interact with the player, that are sent to the
// PlayerService using startForegroundService(), will have SHOULD_START_FOREGROUND_EXTRA, // PlayerService using startForegroundService(), will have SHOULD_START_FOREGROUND_EXTRA,
// to ensure startForeground() is called (otherwise Android will force-crash the app). // to ensure startForeground() is called (otherwise Android will force-crash the app).
if (intent.getBooleanExtra(SHOULD_START_FOREGROUND_EXTRA, false)) { if (intent.getBooleanExtra(SHOULD_START_FOREGROUND_EXTRA, false)) {
final boolean playerWasNull = (player == null); val playerWasNull = (player == null)
if (playerWasNull) { if (playerWasNull) {
// make sure the player exists, in case the service was resumed // make sure the player exists, in case the service was resumed
player = new Player(this, mediaSession, sessionConnector); player = Player(this, mediaSession!!, sessionConnector!!)
} }
// Be sure that the player notification is set and the service is started in foreground, // Be sure that the player notification is set and the service is started in foreground,
@@ -148,107 +150,107 @@ public final class PlayerService extends MediaBrowserServiceCompat {
// no one already and starting the service in foreground should not create any issues. // no one already and starting the service in foreground should not create any issues.
// If the service is already started in foreground, requesting it to be started // If the service is already started in foreground, requesting it to be started
// shouldn't do anything. // shouldn't do anything.
player.UIs().getOpt(NotificationPlayerUi.class) player!!.UIs().get(NotificationPlayerUi::class.java)
.ifPresent(NotificationPlayerUi::createNotificationAndStartForeground); ?.createNotificationAndStartForeground()
if (playerWasNull && onPlayerStartedOrStopped != null) { if (playerWasNull && onPlayerStartedOrStopped != null) {
// notify that a new player was created (but do it after creating the foreground // notify that a new player was created (but do it after creating the foreground
// notification just to make sure we don't incur, due to slowness, in // notification just to make sure we don't incur, due to slowness, in
// "Context.startForegroundService() did not then call Service.startForeground()") // "Context.startForegroundService() did not then call Service.startForeground()")
onPlayerStartedOrStopped.accept(player); onPlayerStartedOrStopped!!.accept(player)
} }
} }
if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction()) if (Intent.ACTION_MEDIA_BUTTON == intent.action &&
&& (player == null || player.getPlayQueue() == null)) { (player == null || player!!.playQueue == null)
) {
/* /*
No need to process media button's actions if the player is not working, otherwise No need to process media button's actions if the player is not working, otherwise
the player service would strangely start with nothing to play the player service would strangely start with nothing to play
Stop the service in this case, which will be removed from the foreground and its Stop the service in this case, which will be removed from the foreground and its
notification cancelled in its destruction notification cancelled in its destruction
*/ */
destroyPlayerAndStopService(); destroyPlayerAndStopService()
return START_NOT_STICKY; return START_NOT_STICKY
} }
if (player != null) { val p = player
player.handleIntent(intent); if (p != null) {
player.UIs().getOpt(MediaSessionPlayerUi.class) p.handleIntent(intent)
.ifPresent(ui -> ui.handleMediaButtonIntent(intent)); p.UIs().get(MediaSessionPlayerUi::class.java)
?.handleMediaButtonIntent(intent)
} }
return START_NOT_STICKY; return START_NOT_STICKY
} }
public void stopForImmediateReusing() { fun stopForImmediateReusing() {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "stopForImmediateReusing() called"); Log.d(TAG, "stopForImmediateReusing() called")
} }
if (player != null && !player.exoPlayerIsNull()) { if (player != null && !player!!.exoPlayerIsNull()) {
// Releases wifi & cpu, disables keepScreenOn, etc. // Releases wifi & cpu, disables keepScreenOn, etc.
// We can't just pause the player here because it will make transition // We can't just pause the player here because it will make transition
// from one stream to a new stream not smooth // from one stream to a new stream not smooth
player.smoothStopForImmediateReusing(); player!!.smoothStopForImmediateReusing()
} }
} }
@Override override fun onTaskRemoved(rootIntent: Intent?) {
public void onTaskRemoved(final Intent rootIntent) { super.onTaskRemoved(rootIntent)
super.onTaskRemoved(rootIntent); if (player != null && !player!!.videoPlayerSelected()) {
if (player != null && !player.videoPlayerSelected()) { return
return;
} }
onDestroy(); onDestroy()
// Unload from memory completely // Unload from memory completely
Runtime.getRuntime().halt(0); Runtime.getRuntime().halt(0)
} }
@Override override fun onDestroy() {
public void onDestroy() {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "destroy() called"); Log.d(TAG, "destroy() called")
} }
super.onDestroy(); super.onDestroy()
cleanup(); cleanup()
mediaBrowserPlaybackPreparer.dispose(); mediaBrowserPlaybackPreparer!!.dispose()
mediaSession.release(); mediaSession!!.release()
mediaBrowserImpl.dispose(); mediaBrowserImpl!!.dispose()
} }
private void cleanup() { private fun cleanup() {
if (player != null) { if (player != null) {
if (onPlayerStartedOrStopped != null) { if (onPlayerStartedOrStopped != null) {
// notify that the player is being destroyed // notify that the player is being destroyed
onPlayerStartedOrStopped.accept(null); onPlayerStartedOrStopped!!.accept(null)
} }
player.destroy(); player!!.saveAndShutdown()
player = null; player = null
} }
// Should already be handled by MediaSessionPlayerUi, but just to be sure. // Should already be handled by MediaSessionPlayerUi, but just to be sure.
mediaSession.setActive(false); mediaSession!!.setActive(false)
// Should already be handled by NotificationUtil.cancelNotificationAndStopForeground() in // Should already be handled by NotificationUtil.cancelNotificationAndStopForeground() in
// NotificationPlayerUi, but let's make sure that the foreground service is stopped. // NotificationPlayerUi, but let's make sure that the foreground service is stopped.
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE); ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
} }
/** /**
* Destroys the player and allows the player instance to be garbage collected. Sets the media * Destroys the player and allows the player instance to be garbage collected. Sets the media
* session to inactive. Stops the foreground service and removes the player notification * session to inactive. Stops the foreground service and removes the player notification
* associated with it. Tries to stop the {@link PlayerService} completely, but this step will * associated with it. Tries to stop the [PlayerService] completely, but this step will
* have no effect in case some service connection still uses the service (e.g. the Android Auto * have no effect in case some service connection still uses the service (e.g. the Android Auto
* system accesses the media browser even when no player is running). * system accesses the media browser even when no player is running).
*/ */
public void destroyPlayerAndStopService() { fun destroyPlayerAndStopService() {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "destroyPlayerAndStopService() called"); Log.d(TAG, "destroyPlayerAndStopService() called")
} }
cleanup(); cleanup()
// This only really stops the service if there are no other service connections (see docs): // This only really stops the service if there are no other service connections (see docs):
// for example the (Android Auto) media browser binder will block stopService(). // for example the (Android Auto) media browser binder will block stopService().
@@ -256,95 +258,96 @@ public final class PlayerService extends MediaBrowserServiceCompat {
// If we were to call stopSelf(), then the service would be surely stopped (regardless of // If we were to call stopSelf(), then the service would be surely stopped (regardless of
// other service connections), but this would be a waste of resources since the service // other service connections), but this would be a waste of resources since the service
// would be immediately restarted by those same connections to perform the queries. // would be immediately restarted by those same connections to perform the queries.
stopService(new Intent(this, PlayerService.class)); stopService(Intent(this, PlayerService::class.java))
} }
@Override override fun attachBaseContext(base: Context?) {
protected void attachBaseContext(final Context base) { super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base))
super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base));
} }
//endregion //endregion
//region Bind //region Bind
@Override override fun onBind(intent: Intent): IBinder? {
public IBinder onBind(final Intent intent) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "onBind() called with: intent = [" + intent Log.d(
+ "], extras = [" + BundleKt.toDebugString(intent.getExtras()) + "]"); TAG,
(
"onBind() called with: intent = [" + intent +
"], extras = [" + intent.extras.toDebugString() + "]"
)
)
} }
if (BIND_PLAYER_HOLDER_ACTION.equals(intent.getAction())) { if (BIND_PLAYER_HOLDER_ACTION == intent.action) {
// Note that this binder might be reused multiple times while the service is alive, even // Note that this binder might be reused multiple times while the service is alive, even
// after unbind() has been called: https://stackoverflow.com/a/8794930 . // after unbind() has been called: https://stackoverflow.com/a/8794930 .
return mBinder; return mBinder
} else if (SERVICE_INTERFACE == intent.action) {
} else if (MediaBrowserServiceCompat.SERVICE_INTERFACE.equals(intent.getAction())) {
// MediaBrowserService also uses its own binder, so for actions related to the media // MediaBrowserService also uses its own binder, so for actions related to the media
// browser service, pass the onBind to the superclass. // browser service, pass the onBind to the superclass.
return super.onBind(intent); return super.onBind(intent)
} else { } else {
// This is an unknown request, avoid returning any binder to not leak objects. // This is an unknown request, avoid returning any binder to not leak objects.
return null; return null
} }
} }
public static class LocalBinder extends Binder { class LocalBinder internal constructor(playerService: PlayerService) : Binder() {
private final WeakReference<PlayerService> playerService; private val playerService: WeakReference<PlayerService?>
LocalBinder(final PlayerService playerService) { init {
this.playerService = new WeakReference<>(playerService); this.playerService = WeakReference<PlayerService?>(playerService)
} }
public PlayerService getService() { val service: PlayerService?
return playerService.get(); get() = playerService.get()
}
}
/**
* @return the current active player instance. May be null, since the player service can outlive
* the player e.g. to respond to Android Auto media browser queries.
*/
@Nullable
public Player getPlayer() {
return player;
} }
/** /**
* Sets the listener that will be called when the player is started or stopped. If a * Sets the listener that will be called when the player is started or stopped. If a
* {@code null} listener is passed, then the current listener will be unset. The parameter taken * `null` listener is passed, then the current listener will be unset. The parameter taken
* by the {@link Consumer} can be null to indicate that the player is stopping. * by the [Consumer] can be null to indicate that the player is stopping.
* @param listener the listener to set or unset * @param listener the listener to set or unset
*/ */
public void setPlayerListener(@Nullable final Consumer<Player> listener) { fun setPlayerListener(listener: Consumer<Player?>?) {
this.onPlayerStartedOrStopped = listener; this.onPlayerStartedOrStopped = listener
if (listener != null) { if (listener != null) {
// if there is no player, then `null` will be sent here, to ensure the state is synced // if there is no player, then `null` will be sent here, to ensure the state is synced
listener.accept(player); listener.accept(player)
} }
} }
//endregion
//endregion
//region Media browser //region Media browser
@Override override fun onGetRoot(
public BrowserRoot onGetRoot(@NonNull final String clientPackageName, clientPackageName: String,
final int clientUid, clientUid: Int,
@Nullable final Bundle rootHints) { rootHints: Bundle?
): BrowserRoot {
// TODO check if the accessing package has permission to view data // TODO check if the accessing package has permission to view data
return mediaBrowserImpl.onGetRoot(clientPackageName, clientUid, rootHints); return mediaBrowserImpl!!.onGetRoot(clientPackageName, clientUid, rootHints)
} }
@Override override fun onLoadChildren(
public void onLoadChildren(@NonNull final String parentId, parentId: String,
@NonNull final Result<List<MediaBrowserCompat.MediaItem>> result) { result: Result<List<MediaBrowserCompat.MediaItem>>
mediaBrowserImpl.onLoadChildren(parentId, result); ) {
mediaBrowserImpl!!.onLoadChildren(parentId, result)
} }
@Override override fun onSearch(
public void onSearch(@NonNull final String query, query: String,
final Bundle extras, extras: Bundle?,
@NonNull final Result<List<MediaBrowserCompat.MediaItem>> result) { result: Result<List<MediaBrowserCompat.MediaItem>>
mediaBrowserImpl.onSearch(query, result); ) {
mediaBrowserImpl!!.onSearch(query, result)
} //endregion
companion object {
private val TAG: String = PlayerService::class.java.getSimpleName()
private val DEBUG = Player.DEBUG
const val SHOULD_START_FOREGROUND_EXTRA: String = "should_start_foreground_extra"
const val BIND_PLAYER_HOLDER_ACTION: String = "bind_player_holder_action"
} }
//endregion
} }

View File

@@ -120,6 +120,14 @@ public final class PlayerHolder {
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 this holders service was already started,
* start playing immediately
* @param newListener set this listener
* */
public void startService(final boolean playAfterConnect, public void startService(final boolean playAfterConnect,
final PlayerServiceExtendedEventListener newListener) { final PlayerServiceExtendedEventListener newListener) {
if (DEBUG) { if (DEBUG) {

View File

@@ -4,7 +4,7 @@ import org.schabi.newpipe.util.GuardedByMutex
import java.util.Optional import java.util.Optional
class PlayerUiList(vararg initialPlayerUis: PlayerUi) { class PlayerUiList(vararg initialPlayerUis: PlayerUi) {
var playerUis = GuardedByMutex(mutableListOf<PlayerUi>()) private var playerUis = GuardedByMutex(mutableListOf<PlayerUi>())
/** /**
* Creates a [PlayerUiList] starting with the provided player uis. The provided player uis * Creates a [PlayerUiList] starting with the provided player uis. The provided player uis
@@ -50,19 +50,19 @@ class PlayerUiList(vararg initialPlayerUis: PlayerUi) {
/** /**
* Destroys all matching player UIs and removes them from the list. * Destroys all matching player UIs and removes them from the list.
* @param playerUiType the class of the player UI to destroy; * @param playerUiType the class of the player UI to destroy, everything if `null`.
* the [Class.isInstance] method will be used, so even subclasses will be * The [Class.isInstance] method will be used, so even subclasses will be
* destroyed and removed * destroyed and removed
* @param T the class type parameter </T> * @param T the class type parameter </T>
* */ * */
fun <T> destroyAll(playerUiType: Class<T?>) { fun <T : PlayerUi> destroyAllOfType(playerUiType: Class<T>? = null) {
val toDestroy = mutableListOf<PlayerUi>() val toDestroy = mutableListOf<PlayerUi>()
// short blocking removal from class to prevent interfering from other threads // short blocking removal from class to prevent interfering from other threads
playerUis.runWithLockSync { playerUis.runWithLockSync {
val new = mutableListOf<PlayerUi>() val new = mutableListOf<PlayerUi>()
for (ui in lockData) { for (ui in lockData) {
if (playerUiType.isInstance(ui)) { if (playerUiType == null || playerUiType.isInstance(ui)) {
toDestroy.add(ui) toDestroy.add(ui)
} else { } else {
new.add(ui) new.add(ui)
@@ -83,7 +83,7 @@ class PlayerUiList(vararg initialPlayerUis: PlayerUi) {
* @param T the class type parameter * @param T the class type parameter
* @return the first player UI of the required type found in the list, or null * @return the first player UI of the required type found in the list, or null
</T> */ </T> */
fun <T> get(playerUiType: Class<T>): T? = fun <T : PlayerUi> get(playerUiType: Class<T>): T? =
playerUis.runWithLockSync { playerUis.runWithLockSync {
for (ui in lockData) { for (ui in lockData) {
if (playerUiType.isInstance(ui)) { if (playerUiType.isInstance(ui)) {
@@ -105,7 +105,7 @@ class PlayerUiList(vararg initialPlayerUis: PlayerUi) {
* [Optional] otherwise * [Optional] otherwise
</T> */ </T> */
@Deprecated("use get", ReplaceWith("get(playerUiType)")) @Deprecated("use get", ReplaceWith("get(playerUiType)"))
fun <T> getOpt(playerUiType: Class<T>): Optional<T & Any> = fun <T : PlayerUi> getOpt(playerUiType: Class<T>): Optional<T> =
Optional.ofNullable(get(playerUiType)) Optional.ofNullable(get(playerUiType))
/** /**

View File

@@ -16,6 +16,7 @@ import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString;
import static org.schabi.newpipe.player.helper.PlayerHelper.nextResizeModeAndSaveToPrefs; import static org.schabi.newpipe.player.helper.PlayerHelper.nextResizeModeAndSaveToPrefs;
import static org.schabi.newpipe.player.helper.PlayerHelper.retrieveSeekDurationFromPreferences; import static org.schabi.newpipe.player.helper.PlayerHelper.retrieveSeekDurationFromPreferences;
import android.annotation.SuppressLint;
import android.content.Intent; import android.content.Intent;
import android.content.res.Resources; import android.content.res.Resources;
import android.graphics.Bitmap; import android.graphics.Bitmap;
@@ -761,7 +762,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
} }
/** /**
* Update the play/pause button ({@link R.id.playPauseButton}) to reflect the action * Update the play/pause button (`R.id.playPauseButton`) to reflect the action
* that will be performed when the button is clicked.. * that will be performed when the button is clicked..
* @param action the action that is performed when the play/pause button is clicked * @param action the action that is performed when the play/pause button is clicked
*/ */
@@ -947,6 +948,8 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
player.toggleShuffleModeEnabled(); player.toggleShuffleModeEnabled();
} }
// TODO: dont reference internal exoplayer2 resources
@SuppressLint("PrivateResource")
@Override @Override
public void onRepeatModeChanged(@RepeatMode final int repeatMode) { public void onRepeatModeChanged(@RepeatMode final int repeatMode) {
super.onRepeatModeChanged(repeatMode); super.onRepeatModeChanged(repeatMode);