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
- 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).
- Construct the player with a Looper you control (e.g. Looper.getMainLooper()) and standardize all call sites on that thread.
- In Robolectric tests, shadow the player's looper and run tasks on it (shadowOf(looper).idle()) rather than calling from the test thread.
- 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
- Construct SimpleBasePlayer subclasses with Looper.getMainLooper() unless you have a dedicated playback thread, then confine all calls to that thread.
- In coroutines use withContext(Dispatchers.Main) (or a custom dispatcher backed by the player's looper) around every player call.
- In Robolectric, idle the player's shadow looper (shadowOf(looper).idle()) instead of calling from the test thread.
- Wrap the player in a facade that funnels every method through a single Handler post.
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
- Missing implementation to handle COMMAND_SET_VOLUME
- Missing implementation to handle COMMAND_SET_DEVICE_VOLUME o
- Missing implementation to handle COMMAND_ADJUST_DEVICE_VOLUM
- Missing implementation to handle COMMAND_SET_VIDEO_SURFACE
- Missing implementation to handle COMMAND_SET_MEDIA_ITEM(S)
AI-assisted analysis of google/ExoPlayer@dd430f7053 (2026-08-14).
Data as JSON: /api/errors/796ae67bd9f27820.
Report an issue: GitHub.