CastContext never restores previous CastState after app returns to foreground
Version
Media3 main branch
More version details
Summary
CastContext stops updating its CastState after the app returns from the background. When the app is backgrounded, the state changes from DEVICE_AVAILABLE to NO_DEVICES_AVAILABLE as expected, but on resume the state never returns to DEVICE_AVAILABLE and no further state callbacks are triggered. This leaves the app stuck in an incorrect cast-state until restart.
We are updating our UI based on this CastState, which is now partially broken.
Reproduce
To reproduce this, use the latest Media3 main branch and run the demo-session variant.
Within the MainActivity, add:
override fun onResume() {
super.onResume()
val snapshot = CastContext.getSharedInstance(this).castState
Log.d("[DEBUG]", "onResume: snapshot $snapshot")
}
And within the MainActivity.onCreate add:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
CastContext.getSharedInstance(this).addCastStateListener { state ->
Log.d("[DEBUG]", "addCastStateListener: $state <-----")
}
Now run the app and make sure that casting devices are available. Steps:
- Observe that the listener prints CastState 2 (DEVICES_AVAILABLE).
- Background the app.
- Observe that the listener prints CastState 1 (NO_DEVICES_AVAILABLE) — as expected.
- Return the app to the foreground.
- Observe that the listener does not print a new state, and the snapshot printed in onResume is still CastState 1 (NO_DEVICES_AVAILABLE).
I might be missing something, but it seems that this listener is not working correctly. Any help is highly appreciated. Keep up the good work, and thanks in advance.
Devices that reproduce the issue
Samsung a51 Android 13 Pixel 7 Android 16
Devices that do not reproduce the issue
No response
Reproducible in the demo app?
Yes
Bug Report
- [ ] You will email the zip file produced by
adb bugreportto [email protected] after filing this issue.
Hi @WSteverink, thanks for reporting. Cast devices are not guaranteed to be available all the time, they are only expected to be discovered while the app is scanning, and scanning is guaranteed to be disabled when the app is in the background (for system resource saving reasons). What I'm trying to say is that this, as is, is not necessarily a bug.
One way of checking if there's actually a bug here is checking whether devices are not available as expected when you press the cast button. The rationale is that the cast button (or rather, the dialog that pops when the cast button is pressed) triggers a route scan, and therefor guarantees that cast devices will be discovered (or if they are not discovered, it's indicative of a bug).
So, I have the following questions for you:
- Are you unable to see any routes in the device picker dialog that shows up when you press the cast button? If they show up, then there's no bug, we are just missing a scan request.
- What are you trying to achieve by accessing the routes directly? There might be a better way of doing what you need to do. Or, at least we can trigger a scan in a way that doesn't eat up unnecessary resources because you are scanning pretty much all the time.
- Are you unable to see any routes in the device picker dialog that shows up when you press the cast button? If they show up, then there's no bug, we are just missing a scan request.
Nope, when i use the CastButton i do see my devices being discovered.
- What are you trying to achieve by accessing the routes directly? There might be a better way of doing what you need to do. Or, at least we can trigger a scan in a way that doesn't eat up unnecessary resources because you are scanning pretty much all the time.
We have a custom design for the CastButton (at the client’s request). We use the CastState from the listener to create our own CastUiState, which we then use to update the UI, such as enabling or disabling buttons. And we use it to decide whether we want to send metadata changes to the player. This works seamlessly with ExoPlayer, but not with CastPlayer.
For now, we could check whether the CastPlayer is being used to determine if casting is active, and fall back to the default CastButton instead of the custom one, since the default button behaves correctly.
Sample code:
val listener = CastStateListener { castState ->
CastUiState(
isCasting = listOf(
CastState.CONNECTED,
CastState.CONNECTING
).contains(castState),
hasDevicesAvailable = castState != CastState.NO_DEVICES_AVAILABLE,
)
}
This works seamlessly with ExoPlayer, but not with CastPlayer
It's unclear to me the relationship between ExoPlayer and the state of the Cast devices available on the network. Particularly, I'm not sure what you mean by seamlessly, since ExoPlayer doesn't know anything about Cast.
For now, we could check whether the CastPlayer is being used to determine if casting is active
If your goal is to know whether you are currently casting, you can use the Player.deviceInfo (and its corresponding listener), and check whether playback is remote or local. There's also the sessionAvailabilityListener in RemoteCastPlayer, but my advise is to stick to the DeviceInfo.
Sample code
Be mindful that the availability of CastDevices depends on the scanning state of the app, which is:
- generally something you want to minimize in order to save resources.
- generally non-reliable: For example, if your app goes into the background your scan requests will be ignored. But they'll be enabled again if the user opens the output switcher while your app is in the background.
So to sum up: if you need to know whether there are available cast devices in your network, you can trigger an active scan request (see CastContext#getMergedSelector and MediaRouter#addCallback), but don't use scan to determine the state or behavior of the cast button. Otherwise, you'll need to be scanning all the time, and then the system / GMS can choose to ignore your scan request as it deems you are scanning spuriously. It's ok to trigger a cast scan while there's a UI that lists Cast devices (i.e. once the user has tapped on the cast button and the media route chooser dialog has popped up).