From d4eb078a52e27b5c8986c0dbed828aa369f2ae21 Mon Sep 17 00:00:00 2001 From: Hans-Peter Lehmann Date: Mon, 13 Jul 2026 00:19:31 +0200 Subject: [PATCH] Fix state of cast device when connecting while service is stopped (#8596) ### Description When freshly binding to the service to start playback (while chromecast is already connected), the service gets started but casting does not transition to PLAYING fast enough. So when unbinding, the service gets destroyed again and releases the session, which stops casting again. So we manually start the service when connected to Chromecast. We also send the media information (in paused state), so that we do not get stuck on the AntennaPod logo. Closes #8581 ### Checklist - [x] I have read the contribution guidelines: https://github.com/AntennaPod/AntennaPod/blob/develop/CONTRIBUTING.md#submit-a-pull-request - [x] I have performed a self-review of my code, going through my changes line by line and carefully considering why this line change is necessary - [x] I have run the automated code checks using `./gradlew checkstyle lint` - [x] My code follows the style guidelines of the AntennaPod project: https://antennapod.org/contribute/develop/app/code-style - [x] I have mentioned the corresponding issue and the relevant keyword (e.g., "Closes: #xy") in the description (see https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) - [x] If it is a core feature, I have added automated tests --- .../playback/base/MediaItemAdapter.java | 8 +-- ...rviceStartingMediaRouteActionProvider.java | 46 ++++++++++++++++ .../cast/src/play/res/menu/cast_button.xml | 2 +- .../service/Media3PlaybackService.java | 54 +++++++++++++++++-- .../service/PlaybackServiceStarter.java | 2 +- 5 files changed, 101 insertions(+), 11 deletions(-) create mode 100644 playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/ServiceStartingMediaRouteActionProvider.java diff --git a/playback/base/src/main/java/de/danoeh/antennapod/playback/base/MediaItemAdapter.java b/playback/base/src/main/java/de/danoeh/antennapod/playback/base/MediaItemAdapter.java index 7782d9491..c6489640f 100644 --- a/playback/base/src/main/java/de/danoeh/antennapod/playback/base/MediaItemAdapter.java +++ b/playback/base/src/main/java/de/danoeh/antennapod/playback/base/MediaItemAdapter.java @@ -37,16 +37,12 @@ public class MediaItemAdapter { * Create a basic media item without attached metadata. * Should be used when initiating playback from outside the service. */ - public static MediaItem fromPlayableStub(Playable playable) { + public static MediaItem fromMediaIdStub(long mediaId) { MediaMetadata.Builder metadataBuilder = new MediaMetadata.Builder(); metadataBuilder.setIsPlayable(true); metadataBuilder.setIsBrowsable(false); - String mediaId = "0"; - if (playable instanceof FeedMedia) { - mediaId = String.valueOf(((FeedMedia) playable).getId()); - } return new MediaItem.Builder() - .setMediaId(mediaId) + .setMediaId(String.valueOf(mediaId)) .setMediaMetadata(metadataBuilder.build()) .build(); } diff --git a/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/ServiceStartingMediaRouteActionProvider.java b/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/ServiceStartingMediaRouteActionProvider.java new file mode 100644 index 000000000..8c824957d --- /dev/null +++ b/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/ServiceStartingMediaRouteActionProvider.java @@ -0,0 +1,46 @@ +package de.danoeh.antennapod.playback.cast; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.mediarouter.app.MediaRouteActionProvider; +import androidx.mediarouter.app.MediaRouteButton; + +/** + * Shows the currently playing episode directly after connecting to the cast device + * by starting the playback service. + */ +public class ServiceStartingMediaRouteActionProvider extends MediaRouteActionProvider { + private static final String TAG = "SrvStartMediaRouteBtn"; + + public ServiceStartingMediaRouteActionProvider(@NonNull Context context) { + super(context); + } + + @NonNull + @Override + public MediaRouteButton onCreateMediaRouteButton() { + return new ServiceStartingMediaRouteButton(getContext()); + } + + private static class ServiceStartingMediaRouteButton extends MediaRouteButton { + ServiceStartingMediaRouteButton(@NonNull Context context) { + super(context); + } + + @Override + public boolean performClick() { + Intent intent = new Intent(); + intent.setComponent(new ComponentName(getContext(), + "de.danoeh.antennapod.playback.service.Media3PlaybackService")); + try { + getContext().startService(intent); + } catch (IllegalStateException e) { + Log.e(TAG, "Unable to start playback service", e); + } + return super.performClick(); + } + } +} diff --git a/playback/cast/src/play/res/menu/cast_button.xml b/playback/cast/src/play/res/menu/cast_button.xml index 6e65bce18..9cc2a324d 100644 --- a/playback/cast/src/play/res/menu/cast_button.xml +++ b/playback/cast/src/play/res/menu/cast_button.xml @@ -5,7 +5,7 @@ diff --git a/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java b/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java index 11a593490..bd09ea3e5 100644 --- a/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java +++ b/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java @@ -1,5 +1,6 @@ package de.danoeh.antennapod.playback.service; +import android.content.Intent; import android.media.audiofx.LoudnessEnhancer; import android.os.Bundle; import android.util.Log; @@ -8,6 +9,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.OptIn; import androidx.core.util.Pair; +import androidx.media3.common.DeviceInfo; import androidx.media3.common.ForwardingPlayer; import androidx.media3.common.MediaItem; import androidx.media3.common.PlaybackException; @@ -183,6 +185,39 @@ public class Media3PlaybackService extends MediaLibraryService { mediaSession = new MediaLibraryService.MediaLibrarySession.Builder(this, player, sessionCallback) .setSessionActivity(new MainActivityStarter(this).withOpenPlayer().getPendingIntent()) .build(); + if (isCasting()) { + keepServiceRunningWhileCasting(); + loadCurrentMediaWhileCasting(); + } + } + + private void loadCurrentMediaWhileCasting() { + long mediaId = PlaybackPreferences.getCurrentlyPlayingFeedMediaId(); + if (mediaId == PlaybackPreferences.NO_MEDIA_PLAYING) { + return; + } + PlaybackController.bindToMedia3Service(this, controller -> { + if (player.getCurrentMediaItem() != null || !isCasting()) { + return; + } + controller.setPlayWhenReady(false); + controller.setMediaItem(MediaItemAdapter.fromMediaIdStub(mediaId)); + controller.prepare(); + }); + } + + /** + * When freshly binding to the service to start playback (while chromecast is already connected), + * the service gets started but casting does not transition to PLAYING fast enough. So when unbinding, + * the service gets destroyed again and releases the session, which stops casting again. + * This method manually starts the service, to be used when connected to chromecast. + */ + private void keepServiceRunningWhileCasting() { + try { + startService(new Intent(this, Media3PlaybackService.class)); + } catch (IllegalStateException e) { + Log.e(TAG, "Unable to keep service running while casting", e); + } } MediaLibrarySessionCallback sessionCallback = new MediaLibrarySessionCallback(this) { @@ -292,6 +327,14 @@ public class Media3PlaybackService extends MediaLibraryService { } } + @Override + public void onDeviceInfoChanged(@NonNull DeviceInfo deviceInfo) { + if (deviceInfo.playbackType == DeviceInfo.PLAYBACK_TYPE_REMOTE) { + keepServiceRunningWhileCasting(); + loadCurrentMediaWhileCasting(); + } + } + @Override public void onMediaItemTransition(@Nullable MediaItem mediaItem, int reason) { if (mediaItem == null) { @@ -425,7 +468,7 @@ public class Media3PlaybackService extends MediaLibraryService { return; } if (needsStreaming(media) && !NetworkUtils.isStreamingAllowed() - && !allowStreamingThisTime) { + && !allowStreamingThisTime && !isCasting()) { showStreamingConfirmation(media); return; } @@ -576,7 +619,8 @@ public class Media3PlaybackService extends MediaLibraryService { && (player.getPlaybackState() == Player.STATE_READY || player.getPlaybackState() == Player.STATE_BUFFERING) && needsStreaming(currentPlayable) - && !NetworkUtils.isStreamingAllowed(); + && !NetworkUtils.isStreamingAllowed() + && !isCasting(); } private boolean handleStreamingConfirmation() { @@ -639,6 +683,10 @@ public class Media3PlaybackService extends MediaLibraryService { startNextInQueue(media, false, true); } + private boolean isCasting() { + return player.getDeviceInfo().playbackType == DeviceInfo.PLAYBACK_TYPE_REMOTE; + } + /** * Loads the next item, and starts it if continuous playback is enabled. */ @@ -671,7 +719,7 @@ public class Media3PlaybackService extends MediaLibraryService { final FeedMedia nextMedia = pair.first; final MediaItem nextMediaItem = pair.second; if (needsStreaming(nextMedia) && !NetworkUtils.isStreamingAllowed() - && !allowStreamingThisTime) { + && !allowStreamingThisTime && !isCasting()) { showStreamingConfirmation(nextMedia); return; } diff --git a/playback/service/src/main/java/de/danoeh/antennapod/playback/service/PlaybackServiceStarter.java b/playback/service/src/main/java/de/danoeh/antennapod/playback/service/PlaybackServiceStarter.java index 0726ab111..d27638a53 100644 --- a/playback/service/src/main/java/de/danoeh/antennapod/playback/service/PlaybackServiceStarter.java +++ b/playback/service/src/main/java/de/danoeh/antennapod/playback/service/PlaybackServiceStarter.java @@ -53,7 +53,7 @@ public class PlaybackServiceStarter { == DeviceInfo.PLAYBACK_TYPE_REMOTE) { controller.play(); // Casting somehow does not play when not quickly starting the old episode } - controller.setMediaItem(MediaItemAdapter.fromPlayableStub(media)); + controller.setMediaItem(MediaItemAdapter.fromMediaIdStub(((FeedMedia) media).getId())); controller.prepare(); controller.play(); });