From 1cdf0a59b13c3dae9bfebb212f1418cdfbfcf069 Mon Sep 17 00:00:00 2001 From: huggenknubbel Date: Tue, 25 Aug 2026 09:30:01 +0200 Subject: [PATCH] cast: report receiver availability from the framework as well ChromecastListener has always implemented CastStateListener, but nothing ever registered it. Whether a receiver was reported as available hung entirely on the media router callback, which does not always fire - and then the cast button never appears even though devices are there. The framework's own signal is registered now and the current state is reported straight away. It is not taken at face value in one direction: the framework drops back to NO_DEVICES_AVAILABLE when a session ends, because it stops discovering at that point, which would hide the button right after someone stops casting. The devices the media router still knows decide that case. The listener is dropped again when the activity goes. --- .../audiobookshelf/app/player/CastManager.kt | 32 +++++++++++++++++++ .../app/plugins/AbsAudioPlayer.kt | 5 +++ 2 files changed, 37 insertions(+) diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/CastManager.kt b/android/app/src/main/java/com/audiobookshelf/app/player/CastManager.kt index 9b3356b6..dd177d85 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/CastManager.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/CastManager.kt @@ -21,6 +21,31 @@ class CastManager constructor(val mainActivity:Activity) { private var playerNotificationService:PlayerNotificationService? = null private var newConnectionListener: SessionListener? = null + private var castStateListener: CastStateListener? = null + + /** + * The framework reports NO_DEVICES_AVAILABLE when a session ends, because it stops discovering - + * taken at face value that hides the cast button. The media router is the second opinion. + */ + private fun isReceiverAvailable(castState: Int): Boolean { + if (castState != CastState.NO_DEVICES_AVAILABLE) return true + + // The framework's own selector, which carries the receiver id this app is configured with - + // building one by hand here would ask about a different receiver than the one being cast to + val selector = getContext().mergedSelector ?: return false + + return getMediaRouter()?.isRouteAvailable( + selector, + MediaRouter.AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE or + MediaRouter.AVAILABILITY_FLAG_REQUIRE_MATCH + ) == true + } + + /** Drops what this manager registered for the activity, which does not outlive it. */ + fun detach() { + castStateListener?.let { getContext().removeCastStateListener(it) } + castStateListener = null + } private fun switchToPlayer(useCastPlayer:Boolean) { Handler(Looper.getMainLooper()).post() { @@ -123,6 +148,13 @@ class CastManager constructor(val mainActivity:Activity) { } fun startRouteScan(connListener:ChromecastListener) { + // ChromecastListener has always implemented CastStateListener, but nothing registered it, so + // availability hung entirely on the media router callback below - which does not always fire + val stateListener = CastStateListener { state -> connListener.onReceiverAvailableUpdate(isReceiverAvailable(state)) } + castStateListener = stateListener + getContext().addCastStateListener(stateListener) + connListener.onReceiverAvailableUpdate(isReceiverAvailable(getContext().castState)) + val callback = object : ScanCallback() { override fun onRouteUpdate(routes: List?) { Log.d(tag, "CAST On ROUTE UPDATED ${routes?.size} | ${getContext().castState}") diff --git a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsAudioPlayer.kt b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsAudioPlayer.kt index b1d80951..6e766377 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsAudioPlayer.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsAudioPlayer.kt @@ -127,6 +127,11 @@ class AbsAudioPlayer : Plugin() { notifyListeners(evtName, ret) } + override fun handleOnDestroy() { + castManager?.detach() + super.handleOnDestroy() + } + override fun handleOnPause() { super.handleOnPause() isInForeground = false