Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

java.lang.IllegalStateException: Future was expected to be done #917

Open
1 task
Kyant0 opened this issue Dec 22, 2023 · 3 comments
Open
1 task

java.lang.IllegalStateException: Future was expected to be done #917

Kyant0 opened this issue Dec 22, 2023 · 3 comments
Assignees
Labels

Comments

@Kyant0
Copy link

Kyant0 commented Dec 22, 2023

Version

Media3 1.2.0

More version details

Using a thread looper rather than main thread in the custom player, it got crashed.
Then delay few milliseconds at the end of service onCreate, it rarely crashed. But this is not a solution.

Is it an intented behavior or I mis-used the threading? I don't want player run on main thread, because it blocks UI sometimes.

Devices that reproduce the issue

Pixel 4 XL (Android 13)

Devices that do not reproduce the issue

No response

Reproducible in the demo app?

Not tested

Reproduction steps

  1. Using the code
class AudioPlaybackService : MediaLibraryService() {
    private var mediaSession: MediaLibrarySession? = null

    private val thread: HandlerThread = HandlerThread("AudioPlaybackService").apply { start() }

    override fun getMainLooper(): Looper {
        return thread.looper
    }

    override fun onCreate() {
        super.onCreate()
        val player = AudioPlayer(mainLooper) // A custom Player
        mediaSession = MediaLibrarySession.Builder(this, player, callback)
    }
}
  1. Run the app and see crash

Expected result

It won't crash at starup

Actual result

java.lang.IllegalStateException: Future was expected to be done: androidx.media3.session.MediaControllerHolder@7887bc2[status=PENDING]
                                                                                                    	at com.google.common.base.Preconditions.checkState(Preconditions.java:590)
	at com.google.common.base.Preconditions.checkState(Preconditions.java:590)
	at com.google.common.util.concurrent.Futures.getDone(Futures.java:1147)
	at androidx.media3.session.MediaNotificationManager.getConnectedControllerForSession(MediaNotificationManager.java:269)
	at androidx.media3.session.MediaNotificationManager.shouldRunInForeground(MediaNotificationManager.java:192)
	at androidx.media3.session.MediaSessionService.onUpdateNotificationInternal(MediaSessionService.java:565)
	at androidx.media3.session.MediaSessionService$MediaSessionListener.onNotificationRefreshRequired(MediaSessionService.java:624)
	at androidx.media3.session.MediaSessionImpl.lambda$onNotificationRefreshRequired$12$androidx-media3-session-MediaSessionImpl(MediaSessionImpl.java:797)
	at androidx.media3.session.MediaSessionImpl$$ExternalSyntheticLambda10.run(D8$$SyntheticClass:0)
	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:7924)
	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)

Media

None

Bug Report

@Kyant0
Copy link
Author

Kyant0 commented Dec 22, 2023

class AudioPlayer(private val service: AudioPlaybackService) : SimpleBasePlayer(service.mainLooper) {

    private val state = object : PlayerState(this) {}

    override fun getState(): State {
        return State.Builder()
            .setAvailableCommands(state.availableCommands)
// Here will crash:
            .setPlayWhenReady(state.playWhenReady, state.playWhenReadyChangeReason)
            .setPlaybackState(state.playbackState)
// Here will crash:
            .setPlaylist(
                service.audioItemTree.getChildren(":songs", 0, Int.MAX_VALUE).map {
                    MediaItemData.Builder(it.mediaId)
                        .setMediaItem(it)
                        .setDurationUs(it.clippingConfiguration.endPositionMs * 1000)
                        .setIsSeekable(true)
                        .build()
                }
            )
            .setCurrentMediaItemIndex(0)
            .build()
    }

    init {
        state.availableCommands = Player.Commands.Builder().addAllCommands().build()
    }

    override fun handleSetPlayWhenReady(playWhenReady: Boolean): ListenableFuture<*> = launchImmediateFuture {
        state.playWhenReady = playWhenReady
        state.playWhenReadyChangeReason = Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST
        state.playbackState = Player.STATE_READY
    }

    override fun handlePrepare(): ListenableFuture<*> = launchImmediateFuture {
        state.playbackState = Player.STATE_BUFFERING
    }

    override fun handleStop(): ListenableFuture<*> = launchImmediateFuture {
        state.playWhenReady = false
        state.playbackState = Player.STATE_IDLE
    }

    override fun handleRelease(): ListenableFuture<*> = launchImmediateFuture {
        state.release()
    }
}

abstract class PlayerState(private val player: Player) {

    var availableCommands = Player.Commands.EMPTY
    var playWhenReady = false
    var playWhenReadyChangeReason: @Player.PlayWhenReadyChangeReason Int =
        Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST
    var playbackState: @Player.State Int = Player.STATE_IDLE

    private val listener = object : Player.Listener {

        override fun onAvailableCommandsChanged(availableCommands: Player.Commands) {
            this@PlayerState.availableCommands = availableCommands
        }

        override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
            this@PlayerState.playWhenReady = playWhenReady
            this@PlayerState.playWhenReadyChangeReason = reason
        }

        override fun onPlaybackStateChanged(playbackState: Int) {
            this@PlayerState.playbackState = playbackState
        }
    }

    init {
        player.addListener(listener)
    }

    fun release() {
        player.removeListener(listener)
    }
}

@marcbaechinger
Copy link
Contributor

Thanks for reporting! From what I see I think you are using threads and the API in a valid way.

My hypothesis is that the player has a state that triggers a code path early that is currently not anticipated by the library. I think Media3 should be able to handle an arbitrary valid player state when the session is built with a Player. So this looks to me like a library bug.

I'm trying to repro with your setup.

The exception seems to be triggered when a bitmap has completed loading and is racing with the media notification controller being connected to the session. It's a bit puzzling that this bitmap completes loading before the media notification controller has connected in which case the Future would be done already. However, we need to anticipate this case and handle this.

Am I right in assuming that you are using an https-URI for the artworkUri of the media item when the first state of the SimpleBasePlayer is built?

@Kyant0
Copy link
Author

Kyant0 commented Dec 22, 2023

No, I am using content scheme for artwork uri

copybara-service bot pushed a commit that referenced this issue Jan 4, 2024
When the media notification controller is requested for a session
with `getConnectedControllerForSession` and the `Future` is not null
but not yet completed, the `Future` was returned either way. This was
reported as creating a race condition between the notification
being requested for update the very first time, and the media
notification controller having completed connecting to the session.

Returning null from `getConnectedControllerForSession` when the
`Future` is available but not yet done fixes the problem. This is
safe because for the case when a notification update is dropped,
the media notification controller will trigger the update as soon
as the connection completes.

Issue: #917
#minor-release
PiperOrigin-RevId: 595699929
microkatz pushed a commit that referenced this issue Jan 11, 2024
When the media notification controller is requested for a session
with `getConnectedControllerForSession` and the `Future` is not null
but not yet completed, the `Future` was returned either way. This was
reported as creating a race condition between the notification
being requested for update the very first time, and the media
notification controller having completed connecting to the session.

Returning null from `getConnectedControllerForSession` when the
`Future` is available but not yet done fixes the problem. This is
safe because for the case when a notification update is dropped,
the media notification controller will trigger the update as soon
as the connection completes.

Issue: #917
#minor-release
PiperOrigin-RevId: 595699929
(cherry picked from commit 5c50b27)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants