Open Kayri opened 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?
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.
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.
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=
.
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.
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)
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?
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!
This issue was solved in my case by using context.applicationContext
instead of using context
directly when creating the MediaController.
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
@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()"
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.
I also still see SecurityException caused by androidx.media3.session.MediaControllerHolder.onRejected as well.
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 aDisposableEffect
which callMediaController.releaseFuture(controllerFuture)
when the component is unload.The logic is made in a VideoPlayerState:
When a user Scroll fast to the bottom the
MediaController.releaseFuture(controllerFuture)
is called andFuture.get()
will throw aCancellationException: "Task was cancelled"
which is the normal behaviour.But if a user scroll up to reload the component video I see in
AbstractFuture.Get()
thatlocalValue = 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.My question is, If I had to understand why the connection is rejected on service side, where should I look?
Thank you