Skip to content

[ui-compose] PresentationState crashes after replacing MediaItem on MediaController during live stream playback #3264

Closed

Description

@Geletinousamigo

Version

Media3 main branch

More version details

No response

Devices that reproduce the issue

Samsung M30s running Android 11

Devices that do not reproduce the issue

No response

Reproducible in the demo app?

Yes

Reproduction steps

Reproduction Steps

  1. Create a MediaSessionService hosting an ExoPlayer.
  2. Connect to it from the UI using a MediaController.
  3. Use Media3 Compose UI (media3-ui-compose) and create a PresentationState using the MediaController.
  4. playback of a live HLS/DASH stream.
  5. Replace the currently playing MediaItem with another live stream URL (e.g. quality switch from Low → Medium).

For reference I am using same MediaSessionService setup as done in AdaptiveJetStream sample, and for me the crash is triggered when I do player.replaceMediaItem

Expected result

The Media plays successfully when switched through player.replaceMediaItem

Actual result

Playback crashses with following stack trace:
java.lang.UnsupportedOperationException at androidx.media3.common.Timeline$RemotableTimeline.getIndexOfPeriod(Timeline.java:1691) at androidx.media3.ui.compose.state.PresentationState.shouldKeepSurfaceVisible(PresentationState.kt:175) at androidx.media3.ui.compose.state.PresentationState.observe$lambda$0(PresentationState.kt:117) at androidx.media3.ui.compose.state.PresentationState$$ExternalSyntheticLambda0.invoke(D8$$SyntheticClass:0) at androidx.media3.common.PlayerListener.onEvents(PlayerExtensions.kt:147) at androidx.media3.session.MediaControllerImplBase.lambda$new$0$androidx-media3-session-MediaControllerImplBase(MediaControllerImplBase.java:185) at androidx.media3.session.MediaControllerImplBase$$ExternalSyntheticLambda86.invoke(D8$$SyntheticClass:0) at androidx.media3.common.util.ListenerSet$ListenerHolder.iterationFinished(ListenerSet.java:477) at androidx.media3.common.util.ListenerSet.handleMessage(ListenerSet.java:421) at androidx.media3.common.util.ListenerSet.$r8$lambda$rFcF5Pkb99AL585p5-2u78YfNkY(Unknown Source:0) at androidx.media3.common.util.ListenerSet$$ExternalSyntheticLambda0.handleMessage(D8$$SyntheticClass:0) at android.os.Handler.dispatchMessage(Handler.java:102) at android.os.Looper.loop(Looper.java:246) at android.app.ActivityThread.main(ActivityThread.java:8653) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:602) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1130)

Media

Not applicable

Bug Report

Activity

  1. self-assigned this
    on Jun 5, 2026
  2. oceanjules commented on Jun 5, 2026

    @oceanjules
    Contributor

    Thanks Nikhil!

    You are right, the bug comes from this chunk

    private fun shouldKeepSurfaceVisible(player: Player): Boolean {
    // Suppress the shutter if transitioning to an unprepared period within the same window. This
    // is necessary to avoid closing the shutter (i.e covering the surface) when such a transition
    // occurs. See: https://github.com/google/ExoPlayer/issues/5507.
    val timeline =
    if (player.isCommandAvailable(Player.COMMAND_GET_TIMELINE)) player.currentTimeline
    else Timeline.EMPTY
    if (timeline.isEmpty) {
    lastPeriodUidWithTracks = null
    return false
    }
    val period = Timeline.Period()
    if (player.isCommandAvailable(Player.COMMAND_GET_TRACKS) && !player.currentTracks.isEmpty) {
    lastPeriodUidWithTracks =
    timeline.getPeriod(player.currentPeriodIndex, period, /* setIds= */ true).uid
    } else
    lastPeriodUidWithTracks?.let {
    val lastPeriodIndexWithTracks = timeline.getIndexOfPeriod(it)
    if (lastPeriodIndexWithTracks != C.INDEX_UNSET) {

    .. which came from the PlayerView:

    @Override
    public void onTracksChanged(Tracks tracks) {
    // Suppress the update if transitioning to an unprepared period within the same window. This
    // is necessary to avoid closing the shutter when such a transition occurs. See:
    // https://github.com/google/ExoPlayer/issues/5507.
    Player player = checkNotNull(PlayerView.this.player);
    Timeline timeline =
    player.isCommandAvailable(COMMAND_GET_TIMELINE)
    ? player.getCurrentTimeline()
    : Timeline.EMPTY;
    if (timeline.isEmpty()) {
    lastPeriodUidWithTracks = null;

    MediaController is operating across an IPC boundary and to prevent exceeding Binder transaction limits, Media3 strips granular data (like Period UIDs) and passes a specialized RemotableTimeline to the controller. By design, RemotableTimeline drops UIDs and explicitly throws an UnsupportedOperationException whenever getIndexOfPeriod(Object uid) is invoked.

    I see 2 ways forward:

    1. Just use the index of the currently playing period (player.currentPeriodIndex)
      If the Compose state is unnecessarily extracting the UID just to find out where we are in the timeline. MediaController natively syncs this value, bypassing the timeline UID lookup entirely and avoiding the crash.

    2. Add a fallback for the MediaController case.

    private fun shouldKeepSurfaceVisible(player: Player): Boolean {
        // ... 
        val index = try {
            player.currentTimeline.getIndexOfPeriod(cachedPeriodUid)
        } catch (e: UnsupportedOperationException) {
            // Fallback for MediaController / RemotableTimeline where UIDs are stripped.
            // We cannot verify precise period identity, so we fallback to checking if the 
            // MediaItem identity has remained the same, or if the player still has 
            // an active video track.
            val hasVideoTrack = player.currentTracks.isTypeSupported(C.TRACK_TYPE_VIDEO)
            val isSameMediaItem = player.currentMediaItem?.mediaId == cachedMediaId
            return isSameMediaItem && hasVideoTrack
        }
        return index != C.INDEX_UNSET
    }

    I will try out different strategies and get back to you with our solution. I'm surprised it hasn't ever crashed on the Views side, because there is nothing that Compose is adding in terms of layering.

  3. locked and limited conversation to collaborators on Sep 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions