google/ExoPlayer · error · IllegalStateException

Player is accessed on the wrong thread. Current thread: '%s'

Error message

Player is accessed on the wrong thread.
Current thread: '%s'
Expected thread: '%s'
See https://developer.android.com/guide/topics/media/issues/player-accessed-on-wrong-thread

What it means

SimpleBasePlayer is single-threaded: every public Player method first calls verifyApplicationThreadAndInitState(), which throws IllegalStateException unless the caller runs on the thread of the applicationLooper supplied in the constructor. The message names the current and expected threads and links to Android's threading guidance. The check also lazily initializes the player State on first access from the correct thread.

Source

Thrown at library/common/src/main/java/com/google/android/exoplayer2/SimpleBasePlayer.java:3517

      listeners.queueEvent(
          Player.EVENT_AVAILABLE_COMMANDS_CHANGED,
          listener -> listener.onAvailableCommandsChanged(newState.availableCommands));
    }
    listeners.flushEvents();
  }

  @EnsuresNonNull("state")
  private void verifyApplicationThreadAndInitState() {
    if (Thread.currentThread() != applicationLooper.getThread()) {
      String message =
          Util.formatInvariant(
              "Player is accessed on the wrong thread.\n"
                  + "Current thread: '%s'\n"
                  + "Expected thread: '%s'\n"
                  + "See https://developer.android.com/guide/topics/media/issues/"
                  + "player-accessed-on-wrong-thread",
              Thread.currentThread().getName(), applicationLooper.getThread().getName());
      throw new IllegalStateException(message);
    }
    if (state == null) {
      // First time accessing state.
      state = getState();
    }
  }

  @RequiresNonNull("state")
  private void updateStateForPendingOperation(
      ListenableFuture<?> pendingOperation, Supplier<State> placeholderStateSupplier) {
    updateStateForPendingOperation(
        pendingOperation,
        placeholderStateSupplier,
        /* seeked= */ false,
        /* isRepeatingCurrentItem= */ false);
  }

  @RequiresNonNull("state")

View on GitHub (pinned to dd430f7053)

Solutions

  1. Route every player interaction onto the looper thread: wrap calls in new Handler(applicationLooper).post(...) (or withContext(Dispatchers.Main) when the player uses the main looper).
  2. Construct the player with a Looper you control (e.g. Looper.getMainLooper()) and standardize all call sites on that thread.
  3. In Robolectric tests, shadow the player's looper and run tasks on it (shadowOf(looper).idle()) rather than calling from the test thread.
  4. For periodic position polling, post a self-re-scheduling Runnable on the same handler instead of polling from another thread.

Example fix

// before
executor.execute(() -> player.seekTo(position)); // wrong thread -> IllegalStateException

// after
new Handler(player.getApplicationLooper()).post(() -> player.seekTo(position));
// or, when the player uses the main looper:
withContext(Dispatchers.Main) { player.seekTo(position) }
Defensive patterns

Strategy: validation

Validate before calling

public void runOnPlayerThread(Player player, Runnable action) {
  if (Thread.currentThread() == player.getApplicationLooper().getThread()) {
    action.run();
  } else {
    new Handler(player.getApplicationLooper()).post(action);
  }
}

Try / catch

// Last-resort guard at boundaries (e.g. library entry points)
try {
  player.pause();
} catch (IllegalStateException e) {
  if (e.getMessage() != null && e.getMessage().contains("wrong thread")) {
    Log.w(TAG, "Player touched off looper thread; reposting", e);
    new Handler(player.getApplicationLooper()).post(player::pause);
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: Invoking any Player method (or creating it and immediately calling getters) from a background thread, a coroutine on Dispatchers.Default, a callback on a different looper, or before the looper thread has started — anywhere Thread.currentThread() != applicationLooper.getThread().

Common situations: Calling player.pause() from a network or media-session callback thread; accessing the player from a non-main coroutine dispatcher when it was built with the main Looper; tests that touch the player on the JUnit thread instead of a ShadowLooper; data-layer code reading player.getCurrentPosition off the UI thread.

Related errors


AI-assisted analysis of google/ExoPlayer@dd430f7053 (2026-08-14). Data as JSON: /api/errors/796ae67bd9f27820. Report an issue: GitHub.