Skip to content

MediaSessionService: the platform session keeps every player command and publishes the whole queue until the session is added, e.g. after a sticky restart #3490

Open

Description

@LordDetson

Version

Media3 pre-release (alpha, beta or RC not in this list)

More version details

Media3 1.12.0-alpha01. Also reproduced with 1.11.1.

Devices that reproduce the issue

  • Android emulator, API 30 (Android 11). Not tried on other devices; nothing in the path described below depends on the API level.

Devices that do not reproduce the issue

No response

Reproducible in the demo app?

Not tested

Reproduction steps

The documented pattern builds the MediaSession in MediaSessionService.onCreate() and returns it from onGetSession(). The platform session takes its commands from the media notification controller, so removing Player.COMMAND_GET_TIMELINE from that controller in MediaSession.Callback.onConnect keeps the queue out of the platform session (1.2.0 release notes).

That restriction only takes effect once MediaSessionService.addSession() has run, because only then does MediaNotificationManager connect the notification controller. Until then the legacy stub keeps DEFAULT_PLAYER_COMMANDS. When the system restarts a sticky service after its process was killed while playing, onStartCommand(null) returns START_STICKY without adding the session, while the platform session is already active. The next headset key then reaches the platform session directly, onPlaybackResumption restores the queue, and the platform session publishes it.

  1. A MediaSessionService that builds its session in onCreate, removes COMMAND_GET_TIMELINE from the media notification controller in onConnect, and restores a queue in onPlaybackResumption.
  2. Play, then adb shell kill -9 <pid> while playing. The system schedules a restart of the crashed service.
  3. Once it has restarted, adb shell input keyevent KEYCODE_MEDIA_PLAY.
  4. adb shell dumpsys media_session

Expected result

The platform session follows the media notification controller's commands from the start, so it never holds the queue, and playback resumed this way runs in the foreground under the media notification.

Calling addSession(session) in onCreate, right after building the session, works around it: LordDetson/piplayer@f502ab7

Actual result

  • The platform session publishes the whole queue (dumpsys media_session shows its size) until any Media3 controller connects; opening the app's activity clears it. With a large queue that is every item on every timeline change.
  • Playback runs without the media notification and without the foreground service.
  • On Android 11 the queue appearing and disappearing again also hits a race in the Bluetooth AVRCP service: MediaPlayerWrapper.isMetadataSynced calls getQueue() twice, and com.android.bluetooth crashed once with a NullPointerException in our runs (fixed in Android 12 by "AVRCP: Cache queue in isMetadataSynced").

Related: #459 (START_STICKY restarts).

Media

Not applicable. An instrumented test that reproduces the state without killing the process (a binding that Media3 does not serve creates the service without addSession): https://github.com/LordDetson/piplayer/blob/main/app/src/androidTest/kotlin/io/github/lorddetson/piplayer/playback/PlatformQueueTest.kt

Bug Report

Activity

  1. marcbaechinger commented on Oct 26, 2026

    @marcbaechinger
    Contributor

    Thanks for reporting! We take a look into this.

    Do I understand correctly this can only be reproduced when the service is restarted after a crash with a media key?

    Can you confirm that the remove timeline command applies correctly in the platform session when:

    a) The service is started normally by building the first controller, that starts the service and binds to it (normal startup)
    b) The service is started by a media key event (for instance BT) to trigger playback resumption without the previous service instance having been crashed.

    As the code path that exposes the wrong behaviour goes through onStartCommand, I'm specifically interested whether b) that goes through onStartCommand works as expected. From my analysis this happens in the failure scenario only after a crash as you describe it. The reason is that addSession is not called correctly in this scenario. Your workaround seems to confirm this.

  2. LordDetson commented on Oct 26, 2026

    @LordDetson
    Author

    Thanks for looking into it! I checked both cases on an API 30 emulator with Media3 1.12.0-alpha01 and the workaround removed (no addSession() in onCreate):

    a) Normal startup, a MediaController that starts and binds the service: COMMAND_GET_TIMELINE removal applies, the platform session has no queue.
    b) A media button intent (ACTION_MEDIA_BUTTON with KEYCODE_MEDIA_PLAY, sent with startForegroundService to a stopped service, no crash before): also correct, playback resumes, the notification is shown and the platform session has no queue.

    Only the restart fails: the system recreates the sticky service with onStartCommand(null), so addSession is never called, and a later media key reaches the already active platform session directly. It does not need a crash as such: any death of the process while the service is started does it (I used adb shell kill -9, as a low-memory kill would), and the key comes from adb shell input keyevent KEYCODE_MEDIA_PLAY after the restart. So I agree with your analysis.

  3. marcbaechinger commented on Oct 26, 2026

    @marcbaechinger
    Contributor

    Thanks a lot for checking! That is inline with what we see.

    In the case you describe, when the system restarts a START_STICKY service and the system passes a null Intent we don't have a media button event nor a connected controller. In the two conventional startup cases (that you tested above), we have either one or the other. What we can do is to create a fallback controller, then call onGetSession(fallbackControllerInfo) and call addSession for the session we get. This requires an app to accept that controller in onConnectAync(session, fallbackControllerInfo) for that controller and the return the new session in onGetSession(fallbackControllerInfo) to allow us to recover from that state. This is the option I see. We will make this controller recognizable in some ways (eg. connection hints) and document this on DAC and in JavaDoc accordingly.

    We are working on a fix for this and will update this issue when we have news. Thanks again for reporting and testing!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions