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
- Move the player interaction onto the main thread: wrap the call in activity.runOnUiThread { ... }, view.post { ... }, or mainHandler.post { ... }
- In coroutines, wrap calls in withContext(Dispatchers.Main) { player.play() }
- 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
- 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
- Centralize all player calls in a single UI-layer wrapper that asserts the main thread
- In coroutines, make PlayerController a main-dispatcher object; annotate functions with @MainThread
- Never call the player from DataSource, Extractor, or codec callbacks — post to the main handler instead
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
- Player is accessed on the wrong thread. Current thread: '%s'
- Could not delete the previous export output file
- Could not create the export output file
- Failed to load MediaPipeShaderProgram
- Unexpected color filter ${colorFilterSelection}
AI-assisted analysis of google/ExoPlayer@dd430f7053 (2026-08-14).
Data as JSON: /api/errors/9502932fc0564108.
Report an issue: GitHub.