{"record":{"id":"33e7c35ae44bd309","repo":"google/ExoPlayer","slug":"player-is-accessed-on-the-wrong-thread-current-th-33e7c3","errorCode":null,"errorMessage":"Player is accessed on the wrong thread.\nCurrent thread: '%s'\nExpected thread: '%s'\nSee https://developer.android.com/guide/topics/media/issues/player-accessed-on-wrong-thread","messagePattern":"Player is accessed on the wrong thread\\.\nCurrent thread: '(.+?)'\nExpected thread: '(.+?)'\nSee https://developer\\.android\\.com/guide/topics/media/issues/player-accessed-on-wrong-thread","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java","lineNumber":2795,"sourceCode":"        throw new IllegalStateException();\n    }\n  }\n\n  private void verifyApplicationThread() {\n    // The constructor may be executed on a background thread. Wait with accessing the player from\n    // the app thread until the constructor finished executing.\n    constructorFinished.blockUninterruptible();\n    if (Thread.currentThread() != getApplicationLooper().getThread()) {\n      String message =\n          Util.formatInvariant(\n              \"Player is accessed on the wrong thread.\\n\"\n                  + \"Current thread: '%s'\\n\"\n                  + \"Expected thread: '%s'\\n\"\n                  + \"See https://developer.android.com/guide/topics/media/issues/\"\n                  + \"player-accessed-on-wrong-thread\",\n              Thread.currentThread().getName(), getApplicationLooper().getThread().getName());\n      if (throwsWhenUsingWrongThread) {\n        throw new IllegalStateException(message);\n      }\n      Log.w(TAG, message, hasNotifiedFullWrongThreadWarning ? null : new IllegalStateException());\n      hasNotifiedFullWrongThreadWarning = true;\n    }\n  }\n\n  private void sendRendererMessage(\n      @C.TrackType int trackType, int messageType, @Nullable Object payload) {\n    for (Renderer renderer : renderers) {\n      if (renderer.getTrackType() == trackType) {\n        createMessageInternal(renderer).setType(messageType).setPayload(payload).send();\n      }\n    }\n  }\n\n  /**\n   * Initializes {@link #keepSessionIdAudioTrack} to keep an audio session ID alive. If the audio\n   * session ID is {@link C#AUDIO_SESSION_ID_UNSET} then a new audio session ID is generated.","sourceCodeStart":2777,"sourceCodeEnd":2813,"githubUrl":"https://github.com/google/ExoPlayer/blob/dd430f7053a1a3958deea3ead6a0565150c06bfc/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java#L2777-L2813","documentation":"Every public ExoPlayer method routes through verifyApplicationThread, which compares the calling thread with the application Looper's thread after the constructor has finished. ExoPlayer is not thread-safe: all access must occur on the single thread that owns the Looper passed to ExoPlayer.Builder(looper). By default (throwsWhenUsingWrongThread=true) an IllegalStateException is thrown with this message; some legacy experimental builds only logged a warning instead.","triggerScenarios":"Calling any player method (play, pause, setMediaItem, release, addListener, ...) from a background thread, an executor, a coroutine on Dispatchers.Default/IO, or a different activity's thread when the player was built on the main Looper.","commonSituations":"Launching playback work in coroutines without withContext(Dispatchers.Main); calling player.release() from a worker thread during teardown; creating the player on a custom HandlerThread but touching it from main; callbacks from SDKs that invoke on their own threads and directly poke the player.","solutions":["Wrap every player interaction in the owner thread: run on the main thread via Activity.runOnUiThread, Handler(Looper.getMainLooper()).post, or withContext(Dispatchers.Main) in coroutines.","If you intentionally use a dedicated playback thread, pass that thread's Looper to ExoPlayer.Builder and access the player only from it.","Marshal third-party callbacks (analytics, DRM, ads SDKs) onto the application Looper before touching the player.","Note Player.Listener callbacks are already invoked on the application thread — chain from them instead of spawning new threads."],"exampleFix":"// before\nscope.launch(Dispatchers.IO) {\n  player.setMediaItem(MediaItem.fromUri(streamUrl))\n  player.play()\n}\n\n// after\nscope.launch {\n  val url = withContext(Dispatchers.IO) { fetchStreamUrl() }\n  player.setMediaItem(MediaItem.fromUri(url)) // Main dispatcher = player's looper\n  player.play()\n}","handlingStrategy":"validation","validationCode":"static void onPlayerThread(Player player, Runnable action) {\n  Looper appLooper = player.getApplicationLooper();\n  if (Looper.myLooper() == appLooper) {\n    action.run();\n  } else {\n  new Handler(appLooper).post(action::run);\n  }\n}","typeGuard":null,"tryCatchPattern":"try {\n  player.play();\n} catch (IllegalStateException e) {\n  // wrong-thread access — do NOT swallow: fix the calling thread instead\n  throw e;\n}","preventionTips":["Create one accessor class that owns all player calls and enforces the application Looper","In coroutines, restrict player calls to withContext(Dispatchers.Main) or a custom Main-player dispatcher","Marshal third-party SDK callbacks through a Handler on the app Looper before touching the player","Enable strict lint/test review for player usage inside background dispatchers"],"tags":["exoplayer","threading","concurrency","looper","illegalstate"],"backgroundTag":null,"analyzedSha":"dd430f7053a1a3958deea3ead6a0565150c06bfc","analyzedAt":"2026-08-14T12:22:02.982Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}