From 46bf78aac48e6ac9a7e2f378b09518c2764dd77c Mon Sep 17 00:00:00 2001 From: Ali Alaei <49282631+aalaei@users.noreply.github.com> Date: Sat, 31 Oct 2026 20:24:07 +0100 Subject: [PATCH] Allow play/pause from untrusted media controllers (#2145) ## Description - From media3 1.11, controllers that aren't trusted get read-only player commands by default. - Google Home sends its commands through com.google.android.apps.mediashell (Google's Cast service on the TV), which isn't trusted, so play/pause from it was dropped. - Logcat showed: MediaSessionLegacyStub: Calling play() omitted due to COMMAND_PLAY_PAUSE not being available - The change: a MediaSession.Callback (TrustAllControllersCallback in PlayerFactory.kt) that gives every controller all player commands again. It uses onConnectAsync and AcceptedResultBuilder(session, controller), so untrusted controllers keep the restricted session commands. - It's applied to the video session (createMediaSession) and the music session (MusicService). - It probably started with the media3 1.11 bump in #1814. ### Related issues - Fixes #2087 ### Testing - JMGO N1S 4K, Google TV, Android 14, ExoPlayer. - Before the fix: play/pause from Google Home did nothing, and logcat showed the "omitted" warning. - After the fix: pause and play from Google Home work. Logcat shows each command reaching handleSetPlayWhenReady, and the warning is gone. - The TV remote still works. - I didn't test music playback; leave that out or test it first. ## Screenshots - None; no UI changes. ## AI or LLM usage - Claude Code read the logcat output, found the media3 1.11 untrusted-controller change, and wrote TrustAllControllersCallback plus the two setCallback(...) calls. --- .../wholphin/services/MusicService.kt | 6 ++++- .../wholphin/services/PlayerFactory.kt | 22 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/MusicService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/MusicService.kt index 4e38ffcc..16ac4edd 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/MusicService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/MusicService.kt @@ -122,7 +122,11 @@ class MusicService mutex.withLock { if (mediaSession == null) { Timber.i("Starting music MediaSession") - mediaSession = MediaSession.Builder(context, player).build() + mediaSession = + MediaSession + .Builder(context, player) + .setCallback(TrustAllControllersCallback) + .build() activityTracker = TrackActivityPlaybackListener(api, player) { state.value.currentItemId?.let { itemId -> diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/PlayerFactory.kt b/app/src/main/java/com/github/damontecres/wholphin/services/PlayerFactory.kt index d61c02ce..a2fd0eb7 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/PlayerFactory.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/PlayerFactory.kt @@ -40,6 +40,8 @@ import com.github.damontecres.wholphin.util.DolbyVisionProfile7ExtractorsFactory import com.github.damontecres.wholphin.util.DolbyVisionProfile7Filter import com.github.damontecres.wholphin.util.Hdr10PlusMaskingFilter import com.github.damontecres.wholphin.util.WholphinDispatchers +import com.google.common.util.concurrent.Futures +import com.google.common.util.concurrent.ListenableFuture import dagger.hilt.android.qualifiers.ApplicationContext import io.github.peerless2012.ass.media.AssHandler import io.github.peerless2012.ass.media.factory.AssRenderersFactory @@ -269,9 +271,29 @@ class PlayerFactory fun createMediaSession(player: Player) = MediaSession .Builder(context, player) + .setCallback(TrustAllControllersCallback) .build() } +/** + * Since media3 1.11, untrusted controllers (e.g. Google Home's Cast bridge, `com.google.android.apps.mediashell`) + * only get read-only player commands by default, so play/pause from them is silently dropped. + * Restore the pre-1.11 behavior of granting all player commands to every controller. + */ +@OptIn(UnstableApi::class) +object TrustAllControllersCallback : MediaSession.Callback { + override fun onConnectAsync( + session: MediaSession, + controller: MediaSession.ControllerInfo, + ): ListenableFuture = + Futures.immediateFuture( + MediaSession.ConnectionResult + .AcceptedResultBuilder(session, controller) + .setAvailablePlayerCommands(MediaSession.ConnectionResult.DEFAULT_PLAYER_COMMANDS) + .build(), + ) +} + val Player.isReleased: Boolean get() { return when (this) {