Skip to content

ForwardingSimpleBasePlayer throws exception due to invalid isLoading state during stop() #2133

Closed

Description

@samaudi

Version

Media3 1.5.1

More version details

No response

Devices that reproduce the issue

Samsung Galaxy Note20 Ultra running Android 13
Samsung S23 Ultra running Android 14

Devices that do not reproduce the issue

No response

Reproducible in the demo app?

Not tested

Reproduction steps

  1. Integrate the library to play video streams using an ExoPlayer instance (which works correctly on its own).
  2. Replace the direct ExoPlayer usage with a ForwardingSimpleBasePlayer without any overrides.
  3. Start playback of a online streaming media item.
  4. Call the stop() method on the player.
  5. Observe the state transitions logged by ExoPlayer via Player.Listener.onEvents(), which report the following sequence:

ExoPlayer State changed. playbackState=STATE_IDLE, isLoading=true
ExoPlayer State changed. playbackState=STATE_IDLE, isLoading=true
ExoPlayer State changed. playbackState=STATE_IDLE, isLoading=false

  1. During these transitions, ForwardingSimpleBasePlayer.getState() passes the playback state and the isLoading flag directly to a SimpleBasePlayer.StateBuilder. When StateBuilder.build() is called, the constructor from SimpleBasePlayer.State() runs a check:
if (builder.playbackState == Player.STATE_IDLE || builder.playbackState == Player.STATE_ENDED) {
    checkArgument(!builder.isLoading, "isLoading only allowed when not in STATE_IDLE or STATE_ENDED");
}
  1. Because the builder receives isLoading=true while the playback state is STATE_IDLE, the check fails and an exception is thrown.

Expected result

The player should update its state without throwing an exception. In particular, when transitioning (e.g., after a stop() call on a stream), the state reported by ForwardingSimpleBasePlayer should correctly handle the isLoading flag—either by ensuring it is false when in STATE_IDLE/STATE_ENDED or by adjusting the check logic—so that no exception occurs.

Actual result

An exception is thrown with the message:
"isLoading only allowed when not in STATE_IDLE or STATE_ENDED"
This occurs because ForwardingSimpleBasePlayer.getState() transfers the playback state and isLoading flag directly to the StateBuilder, and when the underlying ExoPlayer reports STATE_IDLE with isLoading=true during a stop transition, the check in StateBuilder’s constructor fails. Consequently, the ForwardingSimpleBasePlayer becomes unusable in this scenario.

Stacktrace after Player.stop() call:
java.lang.IllegalArgumentException: isLoading only allowed when not in STATE_IDLE or STATE_ENDED
at androidx.media3.common.util.Assertions.checkArgument(Assertions.java:55)
at androidx.media3.common.SimpleBasePlayer$State.(SimpleBasePlayer.java:1027)
at androidx.media3.common.SimpleBasePlayer$State.(SimpleBasePlayer.java:101)
at androidx.media3.common.SimpleBasePlayer$State$Builder.build(SimpleBasePlayer.java:809)
at xxx.xxx.xxx.xxx.xxx.core.player.media3.video.ForwardingSimpleBasePlayer.getState(ForwardingSimpleBasePlayer.java:172)
at androidx.media3.common.SimpleBasePlayer.invalidateState(SimpleBasePlayer.java:3036)
at xxx.xxx.xxx.xxx.xxx.core.player.media3.video.ForwardingSimpleBasePlayer.access$000(ForwardingSimpleBasePlayer.java:27)
at xxx.xxx.xxx.xxx.xxx.core.player.media3.video.ForwardingSimpleBasePlayer$1.onEvents(ForwardingSimpleBasePlayer.java:89)
at androidx.media3.exoplayer.ExoPlayerImpl.lambda$new$0$androidx-media3-exoplayer-ExoPlayerImpl(ExoPlayerImpl.java:296)
at androidx.media3.exoplayer.ExoPlayerImpl$$ExternalSyntheticLambda15.invoke(D8$$SyntheticClass:0)
at androidx.media3.common.util.ListenerSet$ListenerHolder.iterationFinished(ListenerSet.java:353)
at androidx.media3.common.util.ListenerSet.handleMessage(ListenerSet.java:297)
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.loopOnce(Looper.java:226)
at android.os.Looper.loop(Looper.java:313)
at android.app.ActivityThread.main(ActivityThread.java:8762)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:604)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1067)

Media

Not applicable

Bug Report

Activity

  1. self-assigned this
    on Feb 12, 2025
  2. tianyif commented on Feb 12, 2025

    @tianyif
    Contributor

    Hi @samaudi,

    Thanks for reporting! When the stop() is called, ExoPlayer will immediately update the state to IDLE on the application thread and propagate it to the ForwardingSimpleBasePlayer:

    playbackInfo = playbackInfo.copyWithPlaybackState(Player.STATE_IDLE);
    if (error != null) {
    playbackInfo = playbackInfo.copyWithPlaybackError(error);
    }
    pendingOperationAcks++;
    internalPlayer.stop();
    updatePlaybackInfo(
    playbackInfo,
    /* ignored */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
    /* positionDiscontinuity= */ false,
    /* ignored */ DISCONTINUITY_REASON_INTERNAL,
    /* ignored */ C.TIME_UNSET,
    /* ignored */ C.INDEX_UNSET,
    /* repeatCurrentMediaItem= */ false);

    However, the isLoading is updated asynchronously on the playback thread, and it is possible that the update comes late.

    We will be providing a solution for this!

  3. samaudi commented on Feb 12, 2025

    @samaudi
    Author

    Hi @tianyif,

    I have now implemented it in ForwardingSimpleBasePlayer.geState() as follows, and it seems to work so far:

    int playbackState = player.getPlaybackState();
    if (playbackState == Player.STATE_IDLE || playbackState == Player.STATE_ENDED) {
        state.setIsLoading(false);
    } else {
        state.setIsLoading(player.isLoading());
    }
    

    However, I can't quite see what side effects this solution has.

  4. tianyif commented on Mar 21, 2025

    @tianyif
    Contributor

    Fix is available in the latest 1.6.0 prerelease, and will be 1.6.0 stable soon.

  5. locked and limited conversation to collaborators on May 21, 2025
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