androidx / media

Jetpack Media3 support libraries for media use cases, including ExoPlayer, an extensible media player for Android
Apache License 2.0
1.34k stars 315 forks source link

SecurityException with ListenableFuture<MediaController>.Get() #277

Open Kayri opened 1 year ago

Kayri commented 1 year ago

Hello, Currently working on a video feature supporting multiple session for a compose app.

For our example I have 1 video who appear in a middle of a list which contain other items. The feature is fully working when scrolling normally. The video isn't composed yet and when user scroll down the list, when the video appear on view, it create the component which get connected to the mediaSessionService to retrieve or Add a new session. If user continue to scroll down and video is out of the view, we release the component.

The code is simple, I have a Composable containing an AndroidView for the player view and a DisposableEffect which call MediaController.releaseFuture(controllerFuture) when the component is unload.

DisposableEffect(Unit) {
   onDispose { state.onDispose() }
}
AndroidView(
   factory = { _ -> state.view // return the view },
   update = {}
)

The logic is made in a VideoPlayerState:

class VideoPlayerState(...) {
    private val controllerBundle: Bundle = Bundle().apply {...}
    private val controllerFuture: ListenableFuture<MediaController> = MediaController.Builder(
        context,
        SessionToken(context, ComponentName(context, ApolloMediaSessionService::class.java))
    )
        .setConnectionHints(controllerBundle)
        .buildAsync()

    // Todo crash: when scroll fast down and up.
    val controller: MediaController?
        get() = if (controllerFuture.isDone) controllerFuture.get() else null

    init {
        controllerFuture.addListener({ setController() }, MoreExecutors.directExecutor())
    }

    private fun setController() {
        val controller: MediaController = this.controller ?: return
        ....
    }

When a user Scroll fast to the bottom the MediaController.releaseFuture(controllerFuture) is called and Future.get() will throw a CancellationException: "Task was cancelled" which is the normal behaviour.

But if a user scroll up to reload the component video I see in AbstractFuture.Get() that localValue = javax.net.ssl.SSLHandshakeException: Connection closed by peer.

I couldn't go any further in my investigation as limited knowledge on Future. But I understand the issue is coming from MediaSessionService who reject the connection.

FATAL EXCEPTION: main
Process: android.AbcApplication, PID: 5170
java.lang.RuntimeException: java.lang.reflect.InvocationTargetException
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:558)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)
Caused by: java.lang.reflect.InvocationTargetException
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936) 
Caused by: java.util.concurrent.ExecutionException: java.lang.SecurityException: Session rejected the connection request.
    at com.google.common.util.concurrent.AbstractFuture.getDoneValue(AbstractFuture.java:588)
    at com.google.common.util.concurrent.AbstractFuture.get(AbstractFuture.java:547)
    at au.net.abc.apollo.media3.VideoPlayerState.getController(TeaserVideoPlayer.kt:257)
    at au.net.abc.apollo.media3.VideoPlayerState.setController(TeaserVideoPlayer.kt:293)
    at au.net.abc.apollo.media3.VideoPlayerState._init_$lambda$1(TeaserVideoPlayer.kt:288)
    at au.net.abc.apollo.media3.VideoPlayerState.$r8$lambda$j26VTzzHmtGX48tuw1ld6HqiIsc(Unknown Source:0)
    at au.net.abc.apollo.media3.VideoPlayerState$$ExternalSyntheticLambda0.run(Unknown Source:2)
    at com.google.common.util.concurrent.DirectExecutor.execute(DirectExecutor.java:31)
    at com.google.common.util.concurrent.AbstractFuture.executeListener(AbstractFuture.java:1270)
    at com.google.common.util.concurrent.AbstractFuture.complete(AbstractFuture.java:1038)
    at com.google.common.util.concurrent.AbstractFuture.setException(AbstractFuture.java:808)
    at androidx.media3.session.MediaControllerHolder.maybeSetException(MediaControllerHolder.java:67)
    at androidx.media3.session.MediaControllerHolder.onRejected(MediaControllerHolder.java:57)
    at androidx.media3.session.MediaController.release(MediaController.java:512)
    at androidx.media3.session.MediaControllerImplBase$$ExternalSyntheticLambda63.run(Unknown Source:2)
    at androidx.media3.common.util.Util.postOrRun(Util.java:607)
    at androidx.media3.session.MediaController.runOnApplicationLooper(MediaController.java:1837)
    at androidx.media3.session.MediaControllerStub.lambda$onDisconnected$1(MediaControllerStub.java:94)
    at androidx.media3.session.MediaControllerStub$$ExternalSyntheticLambda3.run(Unknown Source:0)
    at androidx.media3.session.MediaControllerStub.lambda$dispatchControllerTaskOnHandler$11(MediaControllerStub.java:301)
    at androidx.media3.session.MediaControllerStub$$ExternalSyntheticLambda11.run(Unknown Source:4)
    at androidx.media3.common.util.Util.postOrRun(Util.java:607)
    at androidx.media3.session.MediaControllerStub.dispatchControllerTaskOnHandler(MediaControllerStub.java:293)
    at androidx.media3.session.MediaControllerStub.onDisconnected(MediaControllerStub.java:92)
    at androidx.media3.session.MediaSessionService$MediaSessionServiceStub.lambda$connect$0$androidx-media3-session-MediaSessionService$MediaSessionServiceStub(MediaSessionService.java:712)
    at androidx.media3.session.MediaSessionService$MediaSessionServiceStub$$ExternalSyntheticLambda0.run(Unknown Source:14)
    at android.os.Handler.handleCallback(Handler.java:942)
    at android.os.Handler.dispatchMessage(Handler.java:99)
    at android.os.Looper.loopOnce(Looper.java:201)
    at android.os.Looper.loop(Looper.java:288)
    at android.app.ActivityThread.main(ActivityThread.java:7872)
    at java.lang.reflect.Method.invoke(Native Method) 
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548) 
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936) 
Caused by: java.lang.SecurityException: Session rejected the connection request.
    at androidx.media3.session.MediaControllerHolder.maybeSetException(MediaControllerHolder.java:67) 
    at androidx.media3.session.MediaControllerHolder.onRejected(MediaControllerHolder.java:57) 
    at androidx.media3.session.MediaController.release(MediaController.java:512) 
    at androidx.media3.session.MediaControllerImplBase$$ExternalSyntheticLambda63.run(Unknown Source:2) 
    at androidx.media3.common.util.Util.postOrRun(Util.java:607) 
    at androidx.media3.session.MediaController.runOnApplicationLooper(MediaController.java:1837) 
    at androidx.media3.session.MediaControllerStub.lambda$onDisconnected$1(MediaControllerStub.java:94) 
    at androidx.media3.session.MediaControllerStub$$ExternalSyntheticLambda3.run(Unknown Source:0) 
    at androidx.media3.session.MediaControllerStub.lambda$dispatchControllerTaskOnHandler$11(MediaControllerStub.java:301) 
    at androidx.media3.session.MediaControllerStub$$ExternalSyntheticLambda11.run(Unknown Source:4) 
    at androidx.media3.common.util.Util.postOrRun(Util.java:607) 
    at androidx.media3.session.MediaControllerStub.dispatchControllerTaskOnHandler(MediaControllerStub.java:293) 
    at androidx.media3.session.MediaControllerStub.onDisconnected(MediaControllerStub.java:92) 
    at androidx.media3.session.MediaSessionService$MediaSessionServiceStub.lambda$connect$0$androidx-media3-session-MediaSessionService$MediaSessionServiceStub(MediaSessionService.java:712) 
    at androidx.media3.session.MediaSessionService$MediaSessionServiceStub$$ExternalSyntheticLambda0.run(Unknown Source:14) 
    at android.os.Handler.handleCallback(Handler.java:942) 
    at android.os.Handler.dispatchMessage(Handler.java:99) 
    at android.os.Looper.loopOnce(Looper.java:201) 
    at android.os.Looper.loop(Looper.java:288) 
    at android.app.ActivityThread.main(ActivityThread.java:7872) 
    at java.lang.reflect.Method.invoke(Native Method) 
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548) 
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936) 

My question is, If I had to understand why the connection is rejected on service side, where should I look?

Thank you

marcbaechinger commented 1 year ago

It looks to me like as if you would release the controller before it actually has connected. Can you confimr this could be the case?

You can verify this by not calling release before buildAsync has completed for a given controller.

I think the code snippet you included doesn't show how you release, but the logic when you release can probably take care of the fact, that the controller potentially isn't yet connected when your code runs in the context of a recycler view.

What I can think of would work is that you put the controller that you build in an async way in a list like pendingControllers and then when the controller has actually arrived in setController remove it from that list. Then if you release, you should wait with releasing before a given controller is not in the pendingController list anymore. If it is still there you need a way to defer releasing until it is actually connected, then release.

You may find better ways to do the above than my suggestion, but I think its clear what I mean.

You could also measure how fast the user is scrolling and at a given speed, stop creating controllers I think. Because you are wasting resources which makes you app not faster that way.

Can you verify if this helps? If this is the root cause you can fix this quickly. We should probably also think about how we can avoid this from the library side but in general you should wait until the controller is returned by buildAsync before actually start using them.

Can you verify whether this helps?

Kayri commented 1 year ago

Thank you for your quick reply.

Yes I thought that could have been another way to solve that issue. I change the code for test purposes to have something like:

init{
    Futures.addCallback(controllerFuture, object : FutureCallback<MediaController>{
            override fun onSuccess(result: MediaController?) {
                Log.d("MEDIA DEBUG", "Init Success")
                controller = result
                setController()
            }

            override fun onFailure(t: Throwable) {
                Log.d("MEDIA DEBUG", "Init FAIL: ${t.message}")
            }

        },MoreExecutors.directExecutor())
}

fun onDispose() {
        Futures.addCallback(controllerFuture, object : FutureCallback<MediaController>{
            override fun onSuccess(result: MediaController?) {
                Log.d("MEDIA DEBUG", "Release Success")
                MediaController.releaseFuture(controllerFuture)
            }

            override fun onFailure(t: Throwable) {
                Log.d("MEDIA DEBUG", " Release FAIL: ${t.message}")
            }

        },MoreExecutors.directExecutor())
}

So there is no crash anymore because I catch the Throwable but I keep getting the same error java.lang.SecurityException: Session rejected the connection request. and that's because the Init Callback and the Release Callback get Success at the same time. My setController() contain different things like addMediaItem which can take time. Therefore a solution would be to also wait the service being completed before releasing the controllerFuture. But that seems not very efficient.

If only I could understand how the connection is working on service side I should be able to see why it reject the connection.

Kayri commented 1 year ago

UPDATE:

For reminder, currently pushing media 3 in a compose environment. I'm having a java.lang.SecurityException: Session rejected the connection request. The app has a feed where you scroll and the goal is to have playable videos sometimes (LazyColumn). I have MediaController.releaseFuture(controllerFuture) inside a DisposableEffect when the item disappear.. When the controller future is not done, it cancels the future. The issue was when I try reconnecting to the session, the controller gets rejected.

I was able to determine where the issue was coming from: MediaSession get() -> Throws: IllegalStateException – if a MediaSession with the same ID already exists in the package. A session is saved inside HashMap SESSION_ID_TO_SESSION_MAP to look at if ID is unique. Therefore, if we cancel the future and the service has had time to build the MediaSession without passing addSession, the ID will be saved internally but will not appear in the list of sessions

Not sure if we should report it as a bug. To temporary solve it I'm simply adding a UUID.Random at the end of the ID and remove it when checking the session Id I need.

marcbaechinger commented 1 year ago

Short answer: I recommend to create the session instance in YourService.onCreate and release is in YourService.release().

Long answer:

Throws: IllegalStateException – if a MediaSession with the same ID already exists in the package.

If the error message is Session ID must be unique. ID= and this is the reason for the problem, I think you can fix this because the library never creates a session, it's the app that creates the session. The app needs to make sure to never create more than one instance with the same session ID. This is not related to controller or service it just when a session with the same ID is created before another instance with the same ID is not yet released. When creation of session and releasing the same happens is the responsibility of the app.

A session is saved inside HashMap SESSION_ID_TO_SESSION_MAP to look at if ID is unique.

Correct. That's happening in the constructor of the MediaSession. It's your service implementation that creates this session. Given I understand correctly and you only want one session instance to live at the same time, I recommend creating this session in YourService.onCreate() once. Create it in onCreate, return always the very same instance in onGetSession(controller) and finally release it in onDestroy() of your service.

If you ever only create a single instance of the session with a given ID you never receive IllegalStateException – Session ID must be unique. ID=.

Generally, the life-cycle of the session has nothing to do with a controller being connected to it or not.

Therefore, if we cancel the future and the service has had time to build the MediaSession without passing addSession, the ID will be saved internally but will not appear in the list of sessions

I'm not sure I understand what you mean with this I'm afraid.

When the controller binds to the service, the service interface binder sent from the session to the controller. When received, the controller calls connect() and sends the IMediaController to the session with which the session can talk back to the controller. Here in connect the service calls your MyService.onGetSession() and adds the session to the list of sessions maintained by the service.

When looking into connect, I don't see that the session ever would not be put into the list of the services:

When inspecting this code, I can't see how calling controller.release too early would not result into putting the service into the list of sessions maintained by the service. Even if that would happen, if your service creates only a single session instance then even if this would be the case, the result wouldn't be a Session ID must be unique. ID=.

Kayri commented 1 year ago

My apologies, I should have give you more details. I'm using the service to support multiple session. Therefore the flow for the service is:

- onGetSession() -> { 
controllerInfo.connectionHints contain a sessionID 
Check if sessions list contain session with this ID ( if yes return session) 
if not, create new mediaSession. MediaSession.Builder(..)... .setId(sessionID).. .build()
return it. 

If a user scroll fast enough the controllerFuture.cancel() is called instead of controller.release(). It's when cancel is called something happen somewhere that avoid the saving of the session.

I agree with what you're saying, I followed the flow in Debug and I can see sessions.put() is being called and I can see my sessions when isSessionAdded is being called. What I found was that onDestroy() is also being called at the end of it. Which I guess is the one responsible to remove the sessions list. (I have looked at removeSession or release but couldn't see how the sessions were remove/released)

So when controllerFuture.get() again, onGetSession is called and inside sessions list is empty. Something here dump the sessions list, but don't dump the SESSION_ID_TO_SESSION_MAP hash-map.

I hope this issue is clearer.

guibirk commented 1 year ago

I'm also getting a Caused by: java.util.concurrent.ExecutionException: java.lang.SecurityException: Session rejected the connection request. It happens when I open the app after I have previously removed it by swiping from the Task Manager. Another condition is that ExoPlayer must be playing something when the app is swiped. If I pause the player before swiping, the exception is not thrown when reopening the app.

The app is a video player that supports local (foreground and background) and chromecast playback. ExoPlayer comes from media3 and is being used inside Compose functions with AndroidView().

I am releasing the Player instances and the MediaSession on the onDestroy() and onTaskRemoved(). Also removing the future on the activity onStop()

I have a PlayerActivity that has:

open class PlayerActivity : AppCompatActivity() {

    private lateinit var mediaControllerFuture: ListenableFuture<MediaController>

    override fun onStart() {
        super.onStart()
        val sessionToken = SessionToken(this, ComponentName(this, PlayerService::class.java))
        mediaControllerFuture = MediaController.Builder(this, sessionToken).buildAsync()
        mediaControllerFuture.addListener(
            {
                //HERE IS WHERE THE EXCEPTION IS THROWN
                playerState.setMediaController(mediaControllerFuture.get())
            },
            MoreExecutors.directExecutor()
        )
    }

    override fun onStop() {
        super.onStop()
        playerState.releaseMediaController()
        MediaController.releaseFuture(mediaControllerFuture)
    }
}

The PlayerService:

class PlayerService : MediaSessionService(), MediaSession.Callback, SessionAvailabilityListener {

    private var mediaSession: MediaSession? = null
    private var castPlayer: CastPlayer? = null
    private lateinit var localPlayer: ExoPlayer

    override fun onCreate() {
        super.onCreate()
        localPlayer = ExoPlayer.Builder(this).build()
        mediaSession = MediaSession
            .Builder(applicationContext, localPlayer)
            .setCallback(this)
            .build()
        CastContext.getSharedInstance(applicationContext, MoreExecutors.directExecutor())
            .addOnCompleteListener {
                castPlayer = CastPlayer(it.result)
                castPlayer?.setSessionAvailabilityListener(this)
            }
    }

    override fun onDestroy() {
        super.onDestroy()
        release()
    }

    override fun onTaskRemoved(rootIntent: Intent?) {
        super.onTaskRemoved(rootIntent)
        release()
    }

    private fun release() {
        localPlayer.release()
        castPlayer?.release()
        mediaSession?.release()
        mediaSession = null
    }

    override
    fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? {
        return mediaSession
    }

    /**
     * Why we need this? Look at the issue below
     * https://github.com/androidx/media/issues/125
     */
    override
    fun onAddMediaItems(
        mediaSession: MediaSession,
        controller: MediaSession.ControllerInfo,
        mediaItems: MutableList<MediaItem>
    ): ListenableFuture<MutableList<MediaItem>> {
        val updatedMediaItems = mediaItems.map {
            it.buildUpon()
                .setUri(it.mediaId)
                .setMimeType(VIDEO_MP4)
                .build()
        }.toMutableList()
        return Futures.immediateFuture(updatedMediaItems)
    }
}

Stacktrace

java.lang.RuntimeException: java.lang.reflect.InvocationTargetException
   at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:558)
   at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)
Caused by: java.lang.reflect.InvocationTargetException
   at java.lang.reflect.Method.invoke(Native Method)
   at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
   at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936) 
Caused by: java.util.concurrent.ExecutionException: java.lang.SecurityException: Session rejected the connection request.
   at com.google.common.util.concurrent.AbstractFuture.getDoneValue(AbstractFuture.java:588)
   at com.google.common.util.concurrent.AbstractFuture.get(AbstractFuture.java:547)
   at com.example.feature.player.PlayerActivity.onStart$lambda$0(PlayerActivity.kt:36)
   at com.example.feature.player.PlayerActivity.$r8$lambda$At_LZVujZqg7QhLdXLfiK96gyKY(Unknown Source:0)
   at com.example.feature.player.PlayerActivity$$ExternalSyntheticLambda0.run(Unknown Source:2)
   at com.google.common.util.concurrent.DirectExecutor.execute(DirectExecutor.java:31)
   at com.google.common.util.concurrent.AbstractFuture.executeListener(AbstractFuture.java:1277)
   at com.google.common.util.concurrent.AbstractFuture.complete(AbstractFuture.java:1038)
   at com.google.common.util.concurrent.AbstractFuture.setException(AbstractFuture.java:808)
   at androidx.media3.session.MediaControllerHolder.maybeSetException(MediaControllerHolder.java:67)
   at androidx.media3.session.MediaControllerHolder.onRejected(MediaControllerHolder.java:57)
   at androidx.media3.session.MediaController.release(MediaController.java:512)
   at androidx.media3.session.MediaControllerImplBase$$ExternalSyntheticLambda63.run(Unknown Source:2)
   at androidx.media3.common.util.Util.postOrRun(Util.java:607)
   at androidx.media3.session.MediaController.runOnApplicationLooper(MediaController.java:1840)
   at androidx.media3.session.MediaControllerStub.lambda$onDisconnected$1(MediaControllerStub.java:94)
   at androidx.media3.session.MediaControllerStub$$ExternalSyntheticLambda3.run(Unknown Source:0)
   at androidx.media3.session.MediaControllerStub.lambda$dispatchControllerTaskOnHandler$11(MediaControllerStub.java:301)
   at androidx.media3.session.MediaControllerStub$$ExternalSyntheticLambda11.run(Unknown Source:4)
   at androidx.media3.common.util.Util.postOrRun(Util.java:607)
   at androidx.media3.session.MediaControllerStub.dispatchControllerTaskOnHandler(MediaControllerStub.java:293)
   at androidx.media3.session.MediaControllerStub.onDisconnected(MediaControllerStub.java:92)
   at androidx.media3.session.MediaSessionService$MediaSessionServiceStub.lambda$connect$0$androidx-media3-session-MediaSessionService$MediaSessionServiceStub(MediaSessionService.java:731)
   at androidx.media3.session.MediaSessionService$MediaSessionServiceStub$$ExternalSyntheticLambda0.run(Unknown Source:14)
   at android.os.Handler.handleCallback(Handler.java:942)
   at android.os.Handler.dispatchMessage(Handler.java:99)
   at android.os.Looper.loopOnce(Looper.java:201)
   at android.os.Looper.loop(Looper.java:288)
   at android.app.ActivityThread.main(ActivityThread.java:7884)
   at java.lang.reflect.Method.invoke(Native Method) 
   at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548) 
   at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)
Caused by: by: java.lang.SecurityException: Session rejected the connection request.
   at androidx.media3.session.MediaControllerHolder.maybeSetException(MediaControllerHolder.java:67) 
   at androidx.media3.session.MediaControllerHolder.onRejected(MediaControllerHolder.java:57) 
   at androidx.media3.session.MediaController.release(MediaController.java:512) 
   at androidx.media3.session.MediaControllerImplBase$$ExternalSyntheticLambda63.run(Unknown Source:2) 
   at androidx.media3.common.util.Util.postOrRun(Util.java:607) 
   at androidx.media3.session.MediaController.runOnApplicationLooper(MediaController.java:1840) 
   at androidx.media3.session.MediaControllerStub.lambda$onDisconnected$1(MediaControllerStub.java:94) 
   at androidx.media3.session.MediaControllerStub$$ExternalSyntheticLambda3.run(Unknown Source:0) 
   at androidx.media3.session.MediaControllerStub.lambda$dispatchControllerTaskOnHandler$11(MediaControllerStub.java:301) 
   at androidx.media3.session.MediaControllerStub$$ExternalSyntheticLambda11.run(Unknown Source:4) 
   at androidx.media3.common.util.Util.postOrRun(Util.java:607) 
   at androidx.media3.session.MediaControllerStub.dispatchControllerTaskOnHandler(MediaControllerStub.java:293) 
   at androidx.media3.session.MediaControllerStub.onDisconnected(MediaControllerStub.java:92) 
   at androidx.media3.session.MediaSessionService$MediaSessionServiceStub.lambda$connect$0$androidx-media3-session-MediaSessionService$MediaSessionServiceStub(MediaSessionService.java:731) 
   at androidx.media3.session.MediaSessionService$MediaSessionServiceStub$$ExternalSyntheticLambda0.run(Unknown Source:14) 
   at android.os.Handler.handleCallback(Handler.java:942) 
   at android.os.Handler.dispatchMessage(Handler.java:99) 
   at android.os.Looper.loopOnce(Looper.java:201) 
   at android.os.Looper.loop(Looper.java:288) 
   at android.app.ActivityThread.main(ActivityThread.java:7884) 
   at java.lang.reflect.Method.invoke(Native Method) 
   at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548) 
   at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936) 
marcbaechinger commented 1 year ago

It happens when I open the app after I have previously removed it by swiping from the Task Manager. Another condition is that ExoPlayer must be playing something when the app is swiped. If I pause the player before swiping, the exception is not thrown when reopening the app.

I think the service is not properly terminated in the case you are describing.

The session demo wants to keep the service running when the task is removed and playback is ongoing:

override fun onTaskRemoved(rootIntent: Intent?) {
    if (!player.playWhenReady) {
      stopSelf()
    }
  }

override fun onDestroy() {
    mediaLibrarySession.release()
    player.release()
    super.onDestroy()
 }

From your code it seems you want to stop the service in any case. Then you can do

override fun onTaskRemoved(rootIntent: Intent?) {
    release();
    stopSelf()
 }

override fun onDestroy() {
    release();
    super.onDestroy()
 }

private fun release() {
    if (!mediaLibrarySession.isRelease()) {
      mediaLibrarySession.release();
      player.release();
   }
}

Can you try if this helps?

guibirk commented 1 year ago

Can you try if this helps?

I forgot stopSelf() inside onTaskRemoved. After adding, as you mentioned, the issue was fixed. I originally thought the service would stop after onTaskRemoved, but I was wrong.

Thanks!

toasterofbread commented 1 year ago

This issue was solved in my case by using context.applicationContext instead of using context directly when creating the MediaController.

https://stackoverflow.com/a/8317874/13979722

BejanCorneliu commented 6 months ago

Hello. Regarding original post i have "Session rejected the connection request." crash

In my code in mController.addListener(...) i use some customCommand. On some phones, inside listener probably mController.get() is not ready From demo code i notice the line : val controller = this.controller ?: return

In my code i do not make this check because I supposed mController.get() ( inside addListener will be ready to go)

My question is : If i put this check line i will get off this crash but what will happend with the user? A new call will be made on that listener when it will be ready ?

Thanks

daniello-fillthegapp commented 5 months ago

@marcbaechinger Hi, I'm having the same problem with:

 override fun onDestroy() {
        mediaSession?.run {
            release()
            player.release()
            mediaSession = null
        }
        super.onDestroy()
    }

    override fun onTaskRemoved(rootIntent: Intent?) {
        mediaSession?.run {
            release()
            if (!player.playWhenReady) {
                player.release()
            }
            stopSelf()
        }
    }

I tried your solution but cannot find method used here: "mediaLibrarySession.isRelease()"

Digipom commented 4 months ago

I'm still seeing "Task was cancelled" on 1.2.1.

Fatal Exception: java.util.concurrent.CancellationException: Task was cancelled.
       at com.google.common.util.concurrent.AbstractFuture.cancellationExceptionWithCause(AbstractFuture.java:1559)
       at com.google.common.util.concurrent.AbstractFuture.getDoneValue(AbstractFuture.java:586)
       at com.google.common.util.concurrent.AbstractFuture.get(AbstractFuture.java:547)
       at com.mycode.core.data.repository.MyRepository.controllerFuture$lambda$1$lambda$0(MyRepository.java:33)
       at android.os.Handler.handleCallback(Handler.java:790)
       at android.os.Handler.dispatchMessage(Handler.java:99)
       at android.os.Looper.loop(Looper.java:164)
       at android.app.ActivityThread.main(ActivityThread.java:6543)
       at java.lang.reflect.Method.invoke(Method.java)
       at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:440)
       at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:810)
private val sessionToken = SessionToken(app, ComponentName(app, MyService::class.java))
    private val controllerFuture = MediaController.Builder(app, sessionToken).buildAsync()
        .apply {
            addListener({
                val controller = get() // CancellationException is here
                controller.addListener(object : Player.Listener {
                    override fun onPlayerErrorChanged(e: PlaybackException?) {
                        error = e
                    }
                })
            }, ContextCompat.getMainExecutor(app))
        }

I'm already using app context here, not activity context. @marcbaechinger Any insights for this one? I'm hesitant to just catch/log the exception since that means the UI would be broken without a connection to the media service. Something else is going on here.

image

I also still see SecurityException caused by androidx.media3.session.MediaControllerHolder.onRejected as well.