google/ExoPlayer · error · IllegalStateException

Not in applications main thread

Error message

Not in applications main thread

What it means

Thrown by Assertions.checkMainThread() when the calling thread's Looper does not match the application's main Looper. ExoPlayer requires most Player method calls (play, pause, seek, release, etc.) to happen on the application thread that created the player, because the player is not thread-safe and its state must be accessed single-threaded. The check only fires when assertions are enabled (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED), so the same bug may silently corrupt state in release builds.

Source

Thrown at library/common/src/main/java/com/google/android/exoplayer2/util/Assertions.java:232

  @EnsuresNonNull({"#1"})
  @Pure
  public static String checkNotEmpty(@Nullable String string, Object errorMessage) {
    if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && TextUtils.isEmpty(string)) {
      throw new IllegalArgumentException(String.valueOf(errorMessage));
    }
    return string;
  }

  /**
   * Throws {@link IllegalStateException} if the calling thread is not the application's main
   * thread.
   *
   * @throws IllegalStateException If the calling thread is not the application's main thread.
   */
  @Pure
  public static void checkMainThread() {
    if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && Looper.myLooper() != Looper.getMainLooper()) {
      throw new IllegalStateException("Not in applications main thread");
    }
  }
}

View on GitHub (pinned to dd430f7053)

Solutions

  1. Move the player interaction onto the main thread: wrap the call in activity.runOnUiThread { ... }, view.post { ... }, or mainHandler.post { ... }
  2. In coroutines, wrap calls in withContext(Dispatchers.Main) { player.play() }
  3. If you intentionally run on a dedicated thread, create the player on that same thread/Looper so its invariant thread matches, and keep all calls there
  4. Audit every player method invocation reachable from callbacks (Player.Listener, DRM callbacks, ImaAdsLoader) and confirm the thread they arrive on before touching the player

Example fix

// before
thread {
    player.seekTo(positionMs) // throws: background thread
}
// after
thread {
    mainHandler.post {
        player.seekTo(positionMs)
    }
}
Defensive patterns

Strategy: validation

Validate before calling

if (Looper.myLooper() != Looper.getMainLooper()) {
  throw new IllegalStateException("Call player APIs on the main thread");
}

Try / catch

Do not catch: this exception marks a threading bug; catching hides state corruption. Fix the call site thread.

Prevention

When it happens

Trigger: Calling any player-facing API annotated to require the main thread from a background/loader/network thread: e.g. calling player.play() from a Runnable on a HandlerThread, from inside runOnUiThread-less callbacks, from a coroutine on Dispatchers.IO/Default, or from ExoPlayer's own analytics/playlist callbacks which are invoked on the playback (app) thread but where the app then hops threads.

Common situations: Doing player control inside.observeForever on a background-lived LiveData, calling player methods from a Thread used for download/DRM work, using coroutines without withContext(ContextCompat.getMainExecutor(...)), or constructing the player with a custom playbackLooper and assuming calls from any thread are safe.

Related errors


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