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
<!-- 
To help us keep the issue tracker clean and work as efficient as
possible,
  please make sure that you have done all of the following.
You can tick the boxes below by placing an x inside the brackets like
this: [x]
-->
- [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
This commit is contained in:
Hans-Peter Lehmann
2026-07-13 00:19:31 +02:00
committed by GitHub
parent 2d714739f7
commit d4eb078a52
5 changed files with 101 additions and 11 deletions

View File

@ -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();
}

View File

@ -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();
}
}
}

View File

@ -5,7 +5,7 @@
<item
android:id="@+id/media_route_menu_item"
android:title=""
app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"
app:actionProviderClass="de.danoeh.antennapod.playback.cast.ServiceStartingMediaRouteActionProvider"
app:showAsAction="always" />
</menu>

View File

@ -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;
}

View File

@ -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();
});